Open
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
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