Chapter 07

Watch the model run

One user action — changing a lead's status to qualified — fires a rule, which fires an action, which starts a saga, which evaluates a decision table and writes rows into three other tables. This chapter follows that chain end to end and shows you where to look when it does not happen.

The chain, before we walk it

#What runsDeclared in the model as
1A user sets status = qualifieda value of %%enum LeadStatus
2leadQualification evaluates%%rule … on Lead event: beforeUpdate
3Its action fires%%action … trigger-workflow when: status == "qualified"
4LeadConversion starts%%workflow … kind: saga trigger: rule
5A decision table sizes the account%%step B Decision
6Account, Contact, Opportunity createdthree %%step … CreateEntity
7The lead is marked converted%%step F UpdateEntity
8The account's tier is stamped%%step G UpdateEntity … targetSource

Walk it

  1. Note where you are starting

    Four accounts, four contacts, four opportunities — the seeded sample data.

    sql
    select count(*) from bus_account;       -- 4
    select count(*) from bus_contact;       -- 4
    select count(*) from bus_opportunity;   -- 4
  2. Create a lead worth converting

    New → fill it in. The values that matter downstream are score 88, which the decision table reads, and an owner, which every created row inherits. Status starts at new.

    A new lead. Seventeen fields, seven required. The form knows which is which from the model's OPTIONAL modifiers.
    The New Lead form filled in for Dana Whitfield at Northwind Trading, with employee count 2500 and annual revenue 75,000,000.
  3. Qualify it

    Edit → set Status to qualified → Save Changes. That is the entire user action.

  4. Count again

    sql
    select count(*) from bus_account;       -- 5  ← new
    select count(*) from bus_contact;       -- 5  ← new
    select count(*) from bus_opportunity;   -- 5  ← new
    select status from bus_lead where email = 'dana.whitfield@northwind.example';
    -- converted
The account the saga created. Type prospect, tier strategic — the decision table's answer for a score of 88 — and the industry carried across from the lead. Account Number is blank because a beforeCreate hook mints it, and that hook is yours to write.
The Accounts list now showing Northwind Trading with account type prospect, tier strategic and industry technology alongside the four seeded accounts.
And the lead itself. Marked converted by step F, in the same transaction that created the three rows. If any step had failed, none of this would exist and the lead would still read qualified.
The lead detail view for Dana Whitfield after conversion.

The receipt

Every run is recorded, with the mutations it applied. This is the first place to look when you want to know what a workflow actually did rather than what you think it did.

sqlsys_workflow_runs
select workflow_name, operation, status, mutations_applied
  from sys_workflow_runs
 order by created_at desc
 limit 1;
jsonmutations_applied, abridged
[
  { "nodeType": "Decision",
    "published": { "accountTier": "strategic",
                   "openingAmount": 250000,
                   "openingAccountType": "prospect" },
    "matchedRows": 1 },

  { "nodeType": "CreateEntity", "table": "bus_account",
    "boundTo": "newAccountId", "createdId": "0f81028e-…" },

  { "nodeType": "CreateEntity", "table": "bus_contact",
    "values": { "account_id": "0f81028e-…", … } },

  { "nodeType": "CreateEntity", "table": "bus_opportunity",
    "values": { "amount": 250000, "stage": "prospecting", … } },

  { "nodeType": "UpdateEntity", "table": "bus_lead",
    "field": "status", "value": "converted", "rowsAffected": 1 },

  { "nodeType": "UpdateEntity", "table": "bus_account",
    "field": "tier", "value": "strategic",
    "matchedOn": "id=0f81028e-…" }
]
Read the last entry again

Step G updated a row created by step C, matched on the id step C published. That is the shared context doing its job — and it is the difference between a workflow that can only touch what it started from and one that can run a process.

The same run list is in the app: open the workflow in the designer and its recent runs are in the sidebar.

Any definition, opened. RenewalPlaybook is the automatic counterpart: it runs on every contract update rather than waiting for a rule, and it is the one saga that deletes as well as creates.
The RenewalPlaybook workflow open in the designer, showing its trigger, steps and details.

When the chain does not fire

Work down this list in order. Each check rules out one link, and the first four cover almost everything.

SymptomLook atUsually
Nothing happened at all select * from sys_workflow_runs order by created_at desc No run means the rule never fired. Check the %%action's when: against the record you actually saved.
Run exists, status failed Its error_details A required column no step supplied. NOT NULL violations here are a modelling bug: mark the column OPTIONAL if a hook mints it, or set it in the step's fields.
Rule triggers the wrong workflow, or none The rule editor's coverage check No catch-all row, so a record fell through the table and matched nothing.
Record stuck on Draft The record's status message A validation-error action or a customValidate hook refused the write. Working as designed — read the message.
A step silently did nothing mutations_applied for that node A Decision that matched no row publishes nothing, and later steps that would have read its variables skip themselves.
Cross-entity step refused The step's directive An entity: with neither targetSource nor targetField. The executor refuses rather than guessing a row.
The backend log [RulesEngine] and [WorkflowService] lines They name the rule, the workflow and the failure verbatim — quicker than any of the above.
Prove it in a test, not by clicking

The generator emits an end-to-end suite alongside the app. bun run test:e2e:fast exercises the API, the rules and the workflows without the bulk-seed volume tests — the fastest way to know a model change did not break a process you already had working.

What to try next

  • Change a threshold. Move the strategic band in step B's decision table from 85 to 95, regenerate, re-seed, convert another lead — the tier changes.
  • Add a step. Give LeadConversion a seventh step that creates an onboarding Activity against the new account, using targetSource: newAccountId.
  • Gate it differently. Change the action's when: to fire only for score >= 70, and watch a low-scoring lead qualify without converting.
  • Write a hook body. Fill in assignAccountNumber in backend/src/modules/hooks/handlers/Account.ts and the blank Account Number column fills itself in.