Build your agent
Workflows
Workflows let you script how an agent responds to common visitor questions before the LLM is involved. Useful for canned answers (refund FAQ, hours, store policies), lead-qualifying questionnaires, branching qualification flows, and direct human handoff on specific keywords.
How it fits
Every visitor message runs through this pipeline:
- Curated answer match — admin-pinned exact-question answer.
- Workflow match ← this page.
- RAG retrieval + LLM generation.
A workflow short-circuits the LLM call when it matches: the visitor receives the scripted bubbles, no token cost is incurred, and the conversation is tagged with the workflow run for analytics.
How a workflow connects to an agent
A workflow only fires for a visitor turn when three conditions all line up:
-
Status is
active. New workflows start indraftand the runtime ignores them entirely until you flip the status — that's how you stage edits without breaking live traffic.disabledis a third state for "paused but not deleted". -
Agent scope matches. On the workflow's
edit page, the Scoped to agent select gives you
two shapes:
-
Workspace-wide (every agent) —
agent_id IS NULL. The workflow is eligible for every agent in your workspace. Good for cross-cutting flows like "the word 'human' anywhere triggers handoff". - Pinned to one agent — pick a specific agent from the dropdown. The workflow is eligible only for conversations on that agent. Good for shop-specific FAQ ("returns" only matters on the Shopify-store agent, not on the docs agent).
-
Workspace-wide (every agent) —
-
Trigger fires. The visitor's message has
to actually match the workflow's keyword set (under the
configured
any/all/exactmatch mode).
Concretely, for every visitor turn the engine runs one query:
SELECT * FROM workflows
WHERE status = 'active'
AND workspace_id = :current_workspace
AND (agent_id IS NULL OR agent_id = :current_agent)
…and walks the matching rows in order, returning the first whose
keywords match the message. Order is the
insertion order from the database (i.e. older workflows
win ties); use status disabled to deactivate a
workflow without losing its keywords. There's no priority
column today — if you need stricter precedence, narrow the
keyword sets so two workflows can't both match the same turn.
Quick recipe — wire a fresh workflow to an agent
- Go to
/app/workflows→ New workflow. - Give it a name. Pick the agent from Scoped to agent (or leave on Workspace-wide).
- Status: pick Active if you're ready to ship it immediately, or Draft while you're still editing.
-
Add at least one keyword in the trigger config (e.g.
refund) and pick a match mode. - Add at least one step — a
messagebubble works. - Save.
- On the targeted agent's public widget, type a message containing the keyword. The workflow runs instead of the LLM, and you'll see the scripted reply.
To stop a workflow without deleting it, flip its status to
disabled. To re-target it from one agent to another,
just change Scoped to agent on the same workflow row —
no need to clone the steps.
Capabilities
- One trigger type: on keyword, with three match modes (any / all / exact).
- Six step types: message, question, branch, tag_lead, webhook, escalate.
- Variable interpolation: {{var_name}} in any message text resolves the captured value at runtime.
- Conditional branching on captured answers (5 match operators + a default fallback).
- Side-effect steps for tagging leads and POSTing to external webhooks (queued, fire-and-forget).
Step types
| Type | What it does |
|---|---|
message |
Send one chat bubble. Supports {{var_name}} interpolation. Walks straight to the next step. |
question |
Send one bubble + pause the run. The visitor's next message is captured into var_name and the run resumes from the next step on the following turn. |
branch |
Evaluate vars[var] against an ordered list of cases and jump to the first matching go_to step index. The case operators are equals, contains, starts_with, is_empty, not_empty, and default (a fallback fired when no other case hits). Loop-guarded at 32 jumps per turn so a malformed graph can't hang the engine. |
tag_lead |
Append string tags to the conversation's Lead row (creates a stub Lead if the visitor hasn't submitted the inline form yet). Tags accumulate and dedupe on fields.tags. Typical use: tag a visitor as pricing_intent after a branch lands them on the Pro lane. |
webhook |
Fire-and-forget POST (or GET) to an external URL with the run's vars, conversation_id, workflow_id, and any extra_payload. Dispatched as a queued DispatchWebhookJob so the visitor's chat surface never waits on a slow integration. Failures land in /admin/jobs/failed after 3 retries. |
escalate |
Send a goodbye bubble + flag the run as escalated. The conversation is now in the inbox for an operator to claim from the takeover UI. |
Trigger match modes
| Mode | Matches when |
|---|---|
any (default) |
At least one keyword appears as a case-insensitive substring of the visitor's message. "pricing" matches "tell me about pricing please". |
all |
Every keyword must appear as a substring. Useful for compound qualifiers — ["enterprise", "security"] won't fire on "tell me about pricing", but will on "enterprise security review". |
exact |
The trimmed lower-cased message equals one of the keywords verbatim. Use for single-word commands ("cancel", "help", "support") that need to NOT fire on substring matches. |
Branch routing
A branch step's cases array is walked in
order. The first case whose match evaluates true wins,
and execution jumps to go_to (a step index).
match: default is a special case that always matches
and should sit last — it's the catch-all when none of the
preceding operators fire.
{
"type": "branch",
"var": "plan_interest",
"cases": [
{ "match": "equals", "value": "free", "go_to": 5 },
{ "match": "contains", "value": "pro", "go_to": 8 },
{ "match": "not_empty", "go_to": 12 },
{ "match": "default", "go_to": 15 }
]
}
A flow can chain branches — the loop-guard caps execution at 32
jumps per turn so two branches accidentally pointing at each other
can't infinite-loop. When the guard trips the run is marked
failed and whatever bubbles were emitted before the
loop are still flushed to the visitor.
Trigger semantics
The runtime walks every active workflow whose
workspace matches the conversation's agent and whose
agent_id is either NULL (workspace-wide) or matches
the conversation's agent. Within those rows, the first workflow
whose keywords match (per its match_mode) wins.
Order on the index page is by updated_at
descending — most-recently-saved workflows are tried first.
Editing — visual canvas (default) and linear form (alt)
Two ways to edit a workflow, both backed by the same JSON shape:
-
Visual canvas at
/app/workflows/{id}/canvas— the default editor. Clicking a workflow on the index page or saving a freshly-created flow lands you here. React-Flow editor with one node per step, draggable connections between handles, and a end-side inspector panel. Branch nodes have one handle per case (plus an optionaldefaulthandle); wire each handle to the next step. -
Linear form at
/app/workflows/{id}/edit— the alternate. A stack of step cards with type-specific fields. Reach it from the canvas page's "Linear edit" button when a flow is simple enough that a flat list reads cleaner than a graph.
The canvas persists the visual layout (node positions + edge
connections) under definition.canvas, alongside the
runtime-consumed definition.steps array — so flows
authored in the canvas re-open with the same arrangement next time.
Saving from the canvas runs a topological walk of the graph from
the trigger node and serialises the steps array in BFS order.
Branch cases[*].go_to are populated from the edge
target indices, so re-saving a flow you only opened to read
leaves the steps unchanged.
Status
- Draft — never matched. Use this while editing.
- Active — matched on every visitor turn.
- Disabled — kept for history but never matched. Useful for seasonal flows you'll re-enable later.
Phase 3 outlook
The persistence shape stays forwards-compatible. Phase 3 will likely add:
- A/B testing — multiple workflow variants share a trigger; the runtime picks one per visitor and records which one converted.
- Per-flow analytics — drop-off rates per step, branch-arm distributions, time-to-handoff.
- Visitor-segment triggers (in addition to keywords) — page URL pattern, returning vs new, GeoIP.
Older runtimes reading a Phase-3 definition will silently skip step types they don't recognise, so a future admin can save a definition that gracefully degrades on an older deployment.