How to build a production support reply system with Vocion
A walkthrough of the v0.4.0 support-reply demo (two operations, one workflow, one card) — historical shape; see the staleness note before copying any YAML.
Staleness note, added 2026-09-08. This post documents
vocion-demos@v0.4.0/ core v0.4.0, and it was accurate then. As of core v2.20.0 (commit1daee5b2), theoperations/{skill.yaml,prompt.md}directory shape and thetype: skillworkflow step shown below are gone:packages/core/src/libs/workspace/loader.tsthrows if a workspace still has anoperations/directory, and the current workflow step schema accepts onlyapprove,ask,action, andsync. Skills now live atskills/<slug>/SKILL.mdand are read by an agent on its own judgement, not invoked as a workflow step — seedocs/entities/workflow.md. The YAML below is preserved as a record of the v0.4.0 shape; it will not load on current core.
The fastest way to understand a framework is to see one real thing built end-to-end with it. This post walks through the support-reply demo that ships with vocion-demos@v0.4.0 — a help-desk triage workflow that turns an inbound ticket into a drafted reply, lands the draft in a human review queue, and "sends" it through a pluggable Card UI.
The demo is small on purpose. Two operations, one workflow, one custom Card, eight seed tickets. About 400 lines of YAML and markdown total. But it exercises every layer the framework cares about: typed operations, durable workflow runs, human-in-the-loop approval, observability traces, and the new pluggable Cards system that landed in this release.
The shape
ticket → summarize_ticket → draft_reply → [approve gate] → send (Card)
(Haiku, ~400ms) (Sonnet, requires approval) (stub envelope)
Three pieces of content under vocion-demos/demos/support-reply/context/support-demo/:
Stale as of core v2.20.0 — the operations/ directory shape was removed; see the note above.
operations/
summarize-ticket/{skill.yaml, prompt.md}
draft-reply/{skill.yaml, prompt.md}
workflows/
support-triage/workflow.yaml
sample-tickets/
T-1001-shipping-delay.md ... T-1008-thank-you.md
No TypeScript required. The runtime loads these on boot and exposes them to the dashboard at /dashboard/workflows/support_triage.
Operation 1 — summarize_ticket
Fast classification. Haiku 4.5, low temperature, no approval required.
Stale as of core v2.20.0 — operations/ no longer loads; see the note above.
# operations/summarize-ticket/skill.yaml
slug: summarize_ticket
name: Summarize Support Ticket
model: claude-haiku-4-5-20251001
temperature: '0.2'
requiresApproval: false
promptFile: prompt.md
inputSchema:
type: object
required: [ticket]
properties:
ticket: { type: string }
customer_name: { type: string }
The prompt is plain markdown with {{input.field}} interpolation:
You are triaging a support ticket. Produce a tight 2-3 sentence summary
that a human support agent could read in 5 seconds.
Cover, in order:
1. The core issue
2. Relevant technical detail
3. Desired resolution
{{#if input.customer_name}}**Customer:** {{input.customer_name}}{{/if}}
**Ticket:**
{{input.ticket}}
That's it. The runtime handles model selection, retries, cost tracking via agent_budget, and Langfuse tracing. Author the prompt, ship the YAML, you have a typed callable operation.
Operation 2 — draft_reply
Same shape, different model. Sonnet 4.6 for the actual drafting, requiresApproval: true so the output lands in the review queue instead of auto-sending.
Stale as of core v2.20.0 — same removed operations/ shape; see the note above.
slug: draft_reply
model: claude-sonnet-4-6
temperature: '0.4'
requiresApproval: true # ← this is the whole HITL story
inputSchema:
required: [summary, ticket]
properties:
summary: { type: string } # output of summarize_ticket
ticket: { type: string } # original, for tone matching
customer_name: { type: string }
sender_name: { type: string }
The prompt has explicit guardrails:
Never promise refunds, escalations to legal, or SLA commitments — those need human approval (this reply lands in the review queue regardless, but don't put words in the company's mouth).
requiresApproval: true is what makes this production-shaped: a draft is generated, captured, traceable, and waiting for a human — never silently sent.
The workflow
Stale as of core v2.20.0 — type: skill is not a valid workflow step on current core; see the note above.
# workflows/support-triage/workflow.yaml
slug: support_triage
inputSchema:
required: [ticket]
properties:
ticket: { type: string }
customer_name: { type: string }
sender_name: { type: string, default: 'the Support team' }
steps:
- name: summary
type: skill
skill: summarize_ticket
input:
ticket: '{{input.ticket}}'
customer_name: '{{input.customer_name}}'
- name: draft
type: skill
skill: draft_reply
input:
summary: '{{steps.summary.output}}'
ticket: '{{input.ticket}}'
customer_name: '{{input.customer_name}}'
sender_name: '{{input.sender_name}}'
- name: review
type: approve
reviews: draft
- name: send
type: action
action: stub.log_only
input:
to_customer: '{{input.customer_name}}'
body_from_step: draft
Four steps. The approve step pauses the run; the action step records intent and emits a structured envelope the new Cards system renders as a "would-be-sent" email card.
The Card
v0.4.0 ships a Cards SDK as the first-class way to render workflow outputs. The send action above emits:
{ __card: 'send-stub',
stubbed: true,
envelope: { to, subject, body, sent_at, action } }
The run-detail page calls resolveCard() on each step's output, which looks up the send-stub first-party Card and renders an email envelope (subject, to, body, "Stub" badge) instead of a JSON blob. Downstream apps can ship their own Cards via PluginManifest.renderers; slug priority resolves collisions so support-reply.sent-email would override send-stub for support-reply runs without touching the framework.
Try it locally
About two minutes from git clone to a streamed workflow run — as of v0.4.0; see the note below before running.
Stale as of core v2.20.0 — these steps do not currently complete. dev.sh will start, but the support_triage workflow will not load: its type: skill steps fail the current workflow step schema, the same removed shape as above. Check the demo's README for current status before running.
git clone --recurse-submodules https://github.com/vocion/vocion-local.git
cd vocion-local
./bootstrap.sh
docker compose up -d
cd vocion-demos/demos/support-reply && ./scripts/dev.sh
Then:
- Open http://localhost:3001/sign-in — the banner autofills
demo@example.com / demo123 - Navigate to
/dashboard/workflows/support_triage - Click Run workflow, paste a ticket body (or copy
T-1001-shipping-delay.md) - Watch the stepper light up: summary → draft → paused for approval
- Approve in
/dashboard/review - Land on the run-detail page; the final step shows the rendered email envelope
Every step has a Langfuse trace; check /dashboard/observability for cost + latency per call.
From stub to production
The demo's action: stub.log_only is intentional — v0.4.0 ships the surface, not the integration. To take it to production, three swaps:
- Real send — replace the stub action with a Gmail/Sendgrid/Zendesk handler. Action handlers are typed; one file in
src/services/temporal/activities/. The Card stays the same; theStubbadge flips toSent. - Cost ceilings — wire an
agent_budgetrow for the demo's account. Hard caps refuse new runs when over budget; soft caps emit a warning. Already available; off by default. - Tenant cards — ship your org's branded sent-email Card via
PluginManifest.renderers. Same Cards SDK; slug priority overrides the genericsend-stubfor your runs.
The full reference guide at /docs/guides/building-a-support-reply-system goes deeper: error handling, multi-tenancy, observability tags, and what changes when you build a different L1 demo from the same template.
Links
- GitHub release: https://github.com/vocion/vocion-core/releases/tag/vocion-v0.4.0
- Demo source: https://github.com/vocion/vocion-demos/tree/main/demos/support-reply
- Starter page: /starters/support-reply
- Cards SDK source:
vocion-core/packages/sdk/src/cards.ts