Skip to content

docs: add Workflows section#9

Open
mpge wants to merge 10 commits intomainfrom
docs/workflows
Open

docs: add Workflows section#9
mpge wants to merge 10 commits intomainfrom
docs/workflows

Conversation

@mpge
Copy link
Copy Markdown
Member

@mpge mpge commented Apr 24, 2026

Summary

Adds `sections/workflows.md` to document the event-driven workflow feature, which was shipped across every framework (reference NestJS + 10 host-framework plugins) but didn't have a dedicated docs page — only passing mentions in `tickets.md` and comparison pages.

The page is wired into `docs.json` between Automations and Followers so the sidebar naturally groups the two automation features together.

What it covers

  • Workflows vs Automations — when to reach for which (reactive vs periodic)
  • Trigger-event catalog — all 14 events workflows can subscribe to
  • Condition reference — status/priority/tag/email/hours_open/etc.
  • Full 12-action catalog — including the `delay` action just shipped with its deferred-job queue semantics, and all three deferred actions from this rollout (`send_webhook`, `assign_round_robin`, `add_follower`)
  • Template interpolation — `{{field}}` variables in canned replies with the "unknown variables stay literal" behavior
  • Round-robin (least-loaded) assignment behavior — not rotating, least current load
  • `send_webhook` payload shape — JSON surface for external integrations
  • Three end-to-end example workflows — VIP auto-tag, chase stale tickets, subject triage
  • Workflow logs + admin UI + scheduler dependency
  • Decision table — when to pick Workflow vs Automation

Why now

The workflow catalog was feature-complete today after the `delay` action landed. That's the natural point to publish the surface-area doc — waiting longer means users continue to miss a full-power feature set that's already shipped in every framework plugin.

Test plan

  • `sections/workflows.md` renders as valid Markdown locally (no unclosed fences, tables align)
  • `docs.json` still parses — new entry sits between automations and followers
  • Reviewer: sanity-check any action descriptions vs the actual implementation (esp. the `delay` queue wording and the round-robin tie-breaker rule)

mpge added 10 commits April 24, 2026 09:09
Covers the event-driven workflow feature that complements the existing
scheduler-driven Automations:

- How workflows differ from Automations (reactive vs periodic)
- Full trigger-event catalog (ticket.created, reply.created, sla.*, etc.)
- Condition reference (status, priority, tag, email, hours_open, ...)
- Action catalog with all 12 actions including the newly-shipped
  `delay` action and its deferred-job queue semantics
- Template variable interpolation for canned replies
- Round-robin (least-loaded) assignment behavior
- send_webhook payload shape
- Three end-to-end example workflows (VIP tagging, chase stale tickets,
  subject triage)
- Workflow logs + admin UI + scheduler dependency notes
- Automations-vs-workflows decision table

Wires the new page into docs.json between the Automations and Followers
entries so the sidebar reflects the relationship.
The previous list invented snake_case variable names
({{ticket_id}}, {{reference_number}}, {{requester_name}},
{{requester_email}}) that don't exist on the NestJS reference ticket
entity. The real interpolator (workflow-engine.service.ts:104) uses
exactly the top-level scalar column names from the framework's schema —
which on NestJS are camelCase (referenceNumber, requesterId, etc.) and
on other frameworks match their local conventions.

Also clarify that add_note does NOT interpolate — only
insert_canned_reply does (in the NestJS executor's
insertCannedReply; addNote writes body verbatim).

Call out that non-scalar relationships (Tag[], Department) are skipped
so readers don't expect {{tag}} or {{department}} to work.
Two factual corrections after cross-checking against the actual
NestJS implementation:

1. send_webhook: I invented a bespoke payload shape with workflow_id,
   flattened ticket fields, snake_case keys. The real shape (from
   WebhookService.dispatch → JSON.stringify({ event, data, timestamp }))
   wraps the full ticket entity under data.ticket. Also documented
   the HMAC-SHA256 signature + X-Escalated-Signature header, which
   matches every other Escalated webhook's delivery semantics.

2. assign_round_robin: I said it accepts a list of agent ids or
   'all agents in department X'. Neither is right. The actual value
   is a single department id; the executor finds all active +
   available AgentProfiles linked to that department and picks the
   one with the fewest open tickets (tie-break by user id for
   determinism). Fixed the description, the action-catalog table
   entry, and the example workflow's assign_round_robin: [3,7,11]
   line to be assign_round_robin: 7 (a department id).
…ually handles

Previously listed 14 trigger events; the WorkflowListener (at
src/listeners/workflow.listener.ts) only wires five:
ticket.created / ticket.updated / ticket.assigned /
ticket.status_changed / reply.created. Everything else I invented
(ticket.priority_changed, ticket.department_changed, ticket.tagged,
ticket.reopened, reply.agent_reply, sla.warning, sla.breached,
inbound.received, signup.invite) either doesn't exist in
ESCALATED_EVENTS or exists but isn't bridged to the Workflow runner.

Also fix the decision table: 'When an agent replies' was pointing at
the nonexistent reply.agent_reply; redirect readers to use reply.created
with an agent-type condition filter.
Three corrections:

1. Conditions are { field, operator, value } triples working on any
   top-level scalar column of the ticket — not named operators like
   has_tag / subject_contains / requester_email_matches. Listed the
   actual 12 operators from WorkflowEngineService.applyOperator.
2. Fixed the intro line that still referenced 'has tag vip' as if it
   were a condition — conditions can't traverse relationships, you'd
   need to flatten tag state onto a ticket column first.
3. Rewrote all four example workflows as JSON matching the actual
   storage format (conditions as array / all / any, actions as typed
   objects) instead of the invented YAML shorthand. Also dropped the
   {{requester_name}} variable that doesn't exist and noted why
   {{referenceNumber}} / {{reference_number}} differ across framework
   ports.
Two wrong claims:

1. 'For each action: status done/skipped/failed and any error message'
   is wrong. The executor catches individual action failures and
   log-warns them but does NOT persist per-action status. The log row
   only stores the entire workflow's error_message (top-level throw)
   if any; otherwise null.

2. 'For deferred actions: a link to the deferred-job row and its
   eventual outcome' is wrong. WorkflowLog has no FK to
   DeferredWorkflowJob; the two tables are independent. Inspect
   escalated_deferred_workflow_jobs directly (status + last_error)
   for delay audit.

Also renamed the 'workflow run' phrasing to 'workflow considered'
since the log row fires even when conditions don't match (matched=false).
The delay action interprets its value as seconds on some frameworks
(NestJS / Spring / WordPress — the ones ported during this rollout,
which chose seconds to match the NestJS reference) and minutes on
others (Laravel / Rails / Django / Adonis / .NET / Go / Phoenix /
Symfony — which shipped delay with the pre-existing 'minutes'
convention before the rollout touched them).

Documented this in the Delayed actions section and annotated both
concrete-number examples (300 for 5 min, 86400 for 24h) with
minutes-equivalents so readers don't accidentally schedule a
60-day wait on Laravel.
Both the Conditions example and the top-level 'Auto-tag VIP tickets'
example filtered on a 'requesterEmail' field. That field is NOT in
the condition map built by WorkflowRunner.ticketToConditionMap —
that map only contains top-level scalar columns, and the ticket
entity's requester_email is a virtual computed-field populated
lazily by TicketService.enrichTickets, which isn't called before
the ticket.created event fires.

Rewrote both examples to use fields that ARE in the condition map
(priority, subject), and added a prominent note in the Conditions
section explaining what IS and ISN'T accessible + how to work around
it (denormalize onto a ticket column, or fan out via a pre-filter
workflow that tags the ticket first).
Claimed the X-Escalated-Signature header carries 'sha256=<hex>'.
The actual value (WebhookService.sign at line 175-177) is just the
hex digest — no prefix.

Also added the sibling X-Escalated-Event header to the docs since
receivers need to know both exist. Spelled out the verification
recipe (recompute hex(hmac_sha256(secret, body)) + timing-safe
equality) so integrators don't have to reverse-engineer it.
The 'When an agent replies' row said to use reply.created with a
condition filtering out customer-authored replies. That doesn't
work: WorkflowListener.onReplyCreated dispatches the TICKET (not
the reply) to the runner, so reply-level fields like author_type
are not in the condition map. Rewrote the row to say the workflow
fires on *any* reply and filtering must happen on the webhook
receiver.

Also replaced the VIP delay row with a 'high-priority delay' row
matching what the earlier conditions example can actually express.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant