ADR-0021: Real-world workflows direction (V9) and the workflow-model extension¶
- Status: Accepted
- Date: 2026-09-16
- Deciders: Jian (product owner)
Context¶
V1–V4 shipped a deliberately narrow single-agent spec: a linear list of steps where each action
step is a model generation, tools fire only from inside a condition step's then/else
(packages/engine/src/workflow.ts is a single linear for over the step list), the only tool
kind is http_request, knowledge_base is in the schema but not executed, and a run starts only
from a manual input. That is far short of what people actually build with LangGraph, Mastra, and
n8n (agentic tool-loops, branching/parallel/loops, RAG, real app connectors, event triggers,
human-in-the-loop, multi-agent).
A planning interview (2026-09-16) chose the near-term direction, prioritized ahead of finishing V5 hosting (V5 is parked as demo-ready until actually deployed). The bet is deliberately scoped, not "do everything".
Decision¶
Direction: Mastra-style structured workflows, single-agent, that plug into the real world.
- Mastra-style, not LangGraph-style, first. Grow the workflow toward Mastra's workflow primitives (sequence, branch, parallel, loops, suspend/resume for human-in-the-loop, data mapping). Mastra is already the export target, so this keeps the canvas↔YAML duality (ADR-0002) and the one-way export / no-lock-in guarantee central. Cyclic graphs and agentic tool-loops (the LangGraph model) are a larger execution-model rebuild, deferred.
- Single-agent, go deep first (ADR-0001 holds). Multi-agent org-charts stay V7.
- North-star hero: support triage & auto-reply — classify an incoming message, look up context via an API, draft a reply, get human approval, send. Every slice below is justified by making this hero real, demoable, and deployable.
- Deployable without the hosted SaaS auth stack. Connector credentials are
${ENV}tokens (e.g.${SLACK_BOT_TOKEN}), the same secrets model as BYOK model keys — no OAuth dance, no user-auth platform required. The deployable artifact is the exported standalone Mastra app (the exporter is extended to emit the webhook listener, connector calls, and the Slack-button approval callback), orkampong serve <spec>for a quick demo. Export stays the deployable artifact, preserving no-lock-in.
The workflow-model extension (Slice A, the load-bearing schema change)¶
Two new workflow step kinds are added to the type-discriminated workflowStepSchema, alongside
the existing action step (no type) and condition step (type: condition):
- Tool step (
type: tool): call a tool as a normal step, not only from anifbranch. - Approval step (
type: approval): a first-class human-in-the-loop pause, not only a condition-branch outcome. Reuses the engine's existing approval/resume machinery.
And data references {{ step.field }} / {{ trigger.field }} / {{ input }}, resolvable in
an action step's query, a tool step's params, and an approval step's message. This is additive
and backward-compatible: existing action/condition steps and the existing {placeholder} URL
resolution keep working; unifying the two reference syntaxes is a later cleanup, not this slice.
The hero workflow in the extended model:
version: "1.0"
agent:
id: support_triage
name: "Support Triage"
role: "Front-line support agent"
goal: "Triage an incoming support message and draft a correct, on-brand reply."
model:
provider: openrouter
name: liquid/lfm-2.5-2.6b:free
api_key: ${OPENROUTER_API_KEY}
tools:
- name: get_order
action: http_request
method: GET
url: "https://api.shop.example/orders/{{ classify.order_id }}"
extract: order
- name: send_reply
action: http_request # Slice B: becomes a Slack/Gmail connector
method: POST
url: "https://api.helpdesk.example/tickets/{{ trigger.ticket_id }}/reply"
workflow:
- step: classify # action step (model): structured output
action: classify
query: "Classify this message. Return { category, urgency, order_id }."
confidence_gate: true
- step: lookup # tool step (new): always runs, not gated on a condition
type: tool
tool: get_order
- step: draft # action step (model), referencing prior outputs
action: generate_text
query: "Draft a reply for a {{ classify.category }} request about {{ lookup.order }}."
- step: review # approval step (new): first-class human gate
type: approval
message: "Approve this {{ classify.category }} reply?"
- step: send # tool step (new)
type: tool
tool: send_reply
Slice sequence (V9, one board card each: KAN-1429..1432)¶
- A. Workflow model core (KAN-1429): the schema/engine/canvas/exporter changes above. Demo: the hero built on the canvas with generic HTTP, run with a pasted message, approved in the canvas modal.
- B. Connectors (KAN-1430): Gmail + Slack connector tool kinds (creds via
${ENV}), keepinghttp_request. Demo: posts to a real Slack channel / sends a real email. - C. Webhook trigger + serve/deploy (KAN-1431):
agent.trigger(webhook first);kampong serve <spec>and the exported app expose the endpoint that starts a run per event. Demo: a real webhook event drives the flow on a deployed process. - D. Headless approval via Slack buttons (KAN-1432): a paused deployed run posts an Approve/Reject Slack message; the click resolves the run. Demo: end-to-end real.
Alternatives considered¶
| Option | Why not (now) |
|---|---|
| LangGraph-style cyclic, stateful, multi-agent graphs first | The most powerful, but the biggest rebuild of the execution model (cycles, agentic tool-loops, supervisor/worker). Deferred; revisit after the Mastra-style single-agent model and the real-world hero are proven. |
| n8n-style integration breadth (hundreds of connectors) first | The moat is breadth, which is a huge, ongoing surface. Ranked second by the product owner; we take the two connectors the hero needs (Gmail, Slack) and the generic HTTP tool rather than chasing breadth up front. |
| Finish V5 hosting before this | Hosting isn't deployed and was explicitly deprioritized. This direction reaches a demoable/deployable product faster and via export, without the multi-tenant auth stack. |
| Full hosted execution for triggers/approval | Not required. kampong serve + the exported app cover deploy without the SaaS auth/RLS stack; connector and approval creds are ${ENV} tokens. |
Consequences¶
- The
AgentSpecgrows new step kinds, a data-reference syntax, connector tool kinds, and anagent.trigger. Each is additive; existing specs keep validating and running. - The exporter becomes the deployment story: it must emit the webhook listener, connector calls, and the Slack approval callback so the exported app is a real deployable service. This deepens the exporter's behavioral-equivalence test surface.
- Headless approval introduces an out-of-band decision channel (Slack interaction payloads), the one genuinely new real-world integration point; it must verify Slack's request signatures.
- V5 hosting cards (KAN-1408/1427/1393/1389) stay open and parked; V9 is sequenced ahead of them.
- Multi-agent (V7), loops/parallel, and RAG execution remain future work; the schema is shaped to admit them without breaking V9 specs.