Skip to content

Guided-flow engine — enablement runbook (ADR-0169)

How to turn the guided-flow engine on, safely. The engine ships gated off (PRs #379–#381); nothing runs until a persona has a flow AND the guidedFlows flag is true for that tenant.

  • Status: runbook
  • Date: 2026-07-04

resolvePersona runs db.select().from(personas), which Drizzle compiles to an explicit column list including flow. If the code deploys to an environment whose personas table lacks the flow column, that SELECT fails → every persona resolution breaks → chat is down in that env.

Therefore migration 0072_persona_flow must be applied to dev + uat + prod BEFORE the engine code deploys — and before the auto-deploy daemon can push main to prod. CI does not run migrations (they are applied by hand).

Correct rollout order:

  1. pnpm migrate:dev · pnpm migrate:uat · pnpm migrate:prod — apply 0072 to all three (additive, nullable → safe on the running app, which ignores the column until deploy).
  2. Merge the stack: #379 → #380 → #381 (and #376/#377/#378 independently).
  3. Let the deploy land. Verify chat still works everywhere — it should be byte-for-byte unchanged (flag off, no persona has a flow).
  4. Only then enable on ONE dev tenant (below).

Rollback is instant at any point (disable the flag / clear the persona flow); the column can stay.

Pick a dev tenant + its default persona:

-- the persona to make guided (the tenant's default)
SELECT id, name FROM personas WHERE tenant_id = '<TENANT_ID>' AND is_default = 1;

1. Give the persona a flow — the Rental Concierge spec:

UPDATE personas
SET flow = '{"draftKind":"reservation","slots":[{"key":"rental_dates","required":true},{"key":"size","required":true},{"key":"delivery","required":false},{"key":"quantity","required":false}],"completion":"draft_delivered","maxTurns":12}'
WHERE id = '<PERSONA_ID>' AND tenant_id = '<TENANT_ID>';

2. Enable the guidedFlows flag for that tenant — a per-tenant override (feature_flags uses value='true' for booleans; updated_at is Drizzle mode:'timestamp' = unix seconds):

INSERT INTO feature_flags (tenant_id, key, value, updated_by, updated_at)
VALUES ('<TENANT_ID>', 'guidedFlows', 'true', 'dogfood', strftime('%s','now'))
ON CONFLICT(tenant_id, key) DO UPDATE SET value = 'true', updated_at = strftime('%s','now');

3. Purge the flags cache (KV ff:<TENANT_ID>, 300s TTL) so the override takes effect immediately instead of after 5 min:

Terminal window
wrangler kv key delete "ff:<TENANT_ID>" --namespace-id <DEV_SESSIONS_KV_ID>

Drive a conversation-backed rental chat (guided flows need a conversationId) via the headless-auth recipe. Then assert:

  1. State persistsconversations.metadata grew a flow key; slots fill turn by turn; status walks eliciting → drafting → delivered:
    SELECT json_extract(metadata,'$.flow.status') AS status,
    json_extract(metadata,'$.flow.slots') AS slots,
    json_extract(metadata,'$.flow.metered') AS metered
    FROM conversations WHERE id = '<CONVERSATION_ID>';
  2. Billing event fires once — exactly one flow.completed row at first Draft delivery:
    SELECT action, after_json, ts FROM audit_log
    WHERE action = 'flow.completed' AND target_id = '<CONVERSATION_ID>';
  3. Behaviour — the bot asked for rental_dates + size before drafting, did not claim a date is available (no date-inventory → it hands off to confirm), and refinements (“different size”) reshape the same Draft without a second flow.completed.

Instant, at any granularity:

  • One tenant: UPDATE feature_flags SET value='false' … (or delete the row) + KV purge.
  • One persona: UPDATE personas SET flow = NULL WHERE id = ….
  • The flow column is additive/nullable — leave it in place.

After the dev dogfood passes: enable guidedFlows in the Business/Enterprise flag-version JSON (so it’s a plan capability, not a per-tenant override), wire the flow.completed audit event into the completed-flow add-on meter (a follow-up slice), and give real personas their flow specs. Do not enable prod before the dev dogfood is green.