Workflow Builder
The Curate-Me Workflow Builder is a visual editor for designing multi-step AI work. You drag nodes onto a canvas, wire them together, run them on a draft (no cost) or publish them to production, and watch every step stream through live with cost, audit, and approval baked in.
Workflows are tenant-scoped (each org sees only its own), version-controlled (publishing freezes a snapshot), and observable end-to-end (every execution writes to workflow_executions with status, duration, and cost).
Onboarding checklist
Use this when you open the builder for the first time. Every step links to the relevant feature below.
- Open
/workflowsin the dashboard - On the empty canvas, pick one of the four poster actions: Start with AI / Use template / Add trigger / Import workflow (see Empty state)
- Add at least one Agent or LLM Call node
- Wire it to an Output node and connect the edges
- Configure any required Variables (declare on the workflow, reference with
{{name}}) - Click Test Draft — runs the workflow in test mode at zero cost
- Click the toolbar validation badge → Fix each issue from the popover (errors block publish)
- Click Publish to freeze a versioned snapshot
- Trigger a real Run and watch the execution stream
- Review cost and failure rate under System → Operations → Workflows (or the palette stats footer’s deep link)
Five minutes from a blank canvas to a published, observable workflow is the target.
Quickstart — Your first workflow
Goal: build a three-node workflow that classifies an incoming message with an LLM and writes the verdict to an output.
- Click New workflow. Name it
quickstart-classifier. - From the left palette, drag a Trigger → Manual node onto the canvas.
- Drag an LLM Call node next to it. Wire the trigger’s output port to the LLM input port.
- Open the LLM node’s config panel and set:
- Model:
claude-haiku-4-5(cheapest classifier) - System prompt:
You classify support tickets as urgent/normal/spam. Return one word. - User prompt:
{{ticket_text}}
- Model:
- Drag an Output node onto the canvas and wire the LLM’s output to it.
- Open the Variables panel (right sidebar) and declare
ticket_textas astringvariable. - Click Test Draft. Provide a test value for
ticket_text(e.g."My server is on fire — production is down") and run. - Inspect the execution stream — you should see the LLM call complete with a token + cost breakdown.
- Click Publish. Your workflow now has a stable ID and version 1.
- Hit Run to fire it for real.
That’s the whole loop. Every advanced feature in this doc layers onto these primitives.
Empty state
When the canvas has zero nodes (a fresh workflow or after deleting every node), the canvas renders an editorial poster overlay with four action cards. The poster sits on a subtle orbital-rings motif and is the recommended starting point — it’s faster than hunting the right palette item for your first node:
| Action | What it does |
|---|---|
| Start with AI | Opens the natural-language workflow generator. Describe the workflow in plain English; the generator drafts a draft DAG you can edit. Featured card with prompt input + suggested chips. |
| Use template | Opens the template gallery (~20 curated templates: classification, ETL, sales research, support triage, etc.). |
| Add trigger | Drops a default Trigger node onto the canvas. Use when you already know the shape of the workflow and just want to start wiring. |
| Import workflow | Navigates to the workflows list page where the JSON import dialog lives. |
The poster auto-hides as soon as the first node is dropped. Panning, scrolling, and drag-drop onto the canvas still work through the overlay (pointer-events: none on the backdrop). Reduced-motion is honored — no entrance animation, no orbital rotation.
Node catalog
The launch-set ships nine node types. They group into four roles: control flow, work, integration, and meta.
| Node | Role | Use when… |
|---|---|---|
| Trigger | Control | You need an entry point. Manual button, webhook, or schedule (cron). |
| Input | Control | You want a typed input port on the canvas (e.g. for testing). |
| Output | Control | The terminal node — collects the final payload returned to caller. |
| LLM Call | Work | A single prompted LLM invocation. The bread and butter of most workflows. |
| Tool Use | Work | Invoke a registered tool (HTTP fetch, MongoDB query, custom function). |
| Agent | Work | A higher-level container running one of the registered agents end-to-end. |
| Condition | Control | Branch on a JSONPath/expression — $.score > 0.7 style. |
| Loop | Control | Iterate over an array. The loop body executes once per item. |
| Approval | Integration | Pause the run until a named approver clicks Approve/Reject in the dashboard. |
| Transform | Work | Reshape data between nodes (pure JSON / template ops, no LLM cost). |
| Error handler | Control | Catch downstream failures and route them to recovery logic. |
| MCP Server / Tool / Resource | Integration | Invoke MCP-served tools — beta, see Beta nodes below. |
| Sticky note | Meta | Documentation on the canvas. No execution cost. |
Each node’s exact configuration schema is rendered in the right-hand inspector when the node is selected. Required fields are marked with an asterisk and block publish until populated.
Beta nodes
Five node types ship as launch-set but flagged Beta because the runtime path is partial or stubbed:
| Node | Why it’s beta |
|---|---|
| Loop | Iterates over an input array; the body-execution path is still being completed. |
| Error handler | Catches upstream errors; downstream recovery flows are being completed. |
| MCP Server | Connector framework wired; some server calls return placeholder data. |
| MCP Tool | Connector framework wired; some tool calls return placeholder data. |
| MCP Resource | Connector framework wired; some resource reads return placeholder data. |
In the palette, beta nodes render a teal Beta badge with a per-node tooltip describing the specific gap. When a workflow contains any beta node, validation emits a uses_beta_node warning at save time — it does not block publish, but it surfaces as a banner on the workflow’s detail page and as an entry in the validation popover (see Validation and cost policies).
The classification lives in apps/dashboard/lib/workflow/node-categories.ts (NODE_LAUNCH_STATUS). Promoting a node to stable happens by editing that map; the backend launch-set validator in services/backend/src/services/workflow_validation.py is kept in sync intentionally.
Variables
Workflows can declare typed variables in the Variables panel:
- Declare a variable with a
name,type(string,number,boolean,json), and optional default. - Reference it anywhere in a config field with
{{name}}syntax — system prompts, user prompts, tool URLs, and condition expressions all support template interpolation. - Variables are resolved per execution. Manual runs prompt you for values; webhook + schedule runs receive them from the trigger payload.
{
"variables": [
{ "name": "ticket_text", "type": "string", "required": true },
{ "name": "min_confidence", "type": "number", "default": 0.7 }
]
}Secrets (API keys, webhook secrets) should be stored in the org’s Settings → Secrets vault and referenced by name. They never appear in the canvas JSON.
Inspector
Selecting any node opens the right-hand inspector. Six tabs cover the full authoring + debugging surface:
| Tab | What it shows |
|---|---|
| Config | Schema-driven controls for the node’s configuration. Required fields marked with an asterisk; invalid values surface inline. |
| Inputs | Typed input ports with their resolved types. Each row click-throughs to the upstream producer node. |
| Outputs | Typed output ports + downstream consumer count + a preview of the value emitted on the most recent execution. |
| Runs | Per-node execution history (latest 10 across recent workflow runs). Each row is expandable to show the raw event payload — useful for diffing flaky runs. |
| Policy | Org-governance view: model allowlist, HITL thresholds, cost limits, PII/tool restrictions that apply to this node. Read-only here; edited under Governance Policies. |
| JSON | Editable JSON view of the node’s full config. Secret-looking fields are masked (••••) by default; reveal-with-confirm exposes them for a short window. Edits round-trip back through the schema validator before persisting. |
Tabs are sticky across selections, so debugging multiple nodes in turn (e.g. cycling through a failing branch) keeps you on the same tab.
Triggers and run modes
A workflow can be invoked four different ways. Each one is wired through the same execution engine and produces an identical workflow_executions record.
Manual
Click Run in the dashboard. The dashboard prompts for any declared variables and starts the execution. Useful for ad-hoc work and demoing.
Webhook
Each published workflow gets a stable webhook URL of the form:
POST https://api.curate-me.ai/api/v1/admin/workflows/{workflow_id}/webhook
X-CM-Webhook-Secret: <generated-per-workflow>
Content-Type: application/json
{
"input_data": { "ticket_text": "Production is on fire" }
}The webhook secret is rotated from the workflow’s Settings → Webhook tab. Requests without it are rejected at the door (no governance chain spent).
Schedule
Cron-style scheduling. Configure under Settings → Schedule on a published workflow. The scheduler enforces tenant isolation — schedules cannot fire workflows owned by another org even if the cron is misconfigured.
Test Draft vs Publish vs Run
| Action | Spends cost? | Persists execution? | Validation? |
|---|---|---|---|
| Test Draft | No — runs in test mode, mocks LLM responses by default | Marked test=true, ignored by analytics | Full validation runs |
| Publish | No — doesn’t execute, freezes a version | No execution row | Hard-blocks on validation errors |
| Run | Yes — real LLM calls, real tool side-effects | Yes — feeds the observability dashboard | Validation re-runs at start; cost policy can still halt the run |
Test Draft is the right answer 95% of the time during development. Reserve Run for the moment you actually want the side effects.
Approvals
Drop an Approval node anywhere downstream of a Trigger to require human sign-off mid-run.
Configure on the node:
approvers— list of user IDs or group IDs that can approve.timeout_seconds— auto-cancel after this many seconds with no decision.cost_threshold_usd— optional, only require approval when accumulated cost exceeds this.
Runtime behavior:
- Execution reaches the Approval node and transitions to
paused. - All listed approvers receive a notification (Slack / Teams / email — whichever channels their org has wired).
- The run is visible in Approvals in the dashboard with full input/output context up to that point.
- An approver clicks Approve (with optional comment + modified output) or Reject.
- On approve, the workflow resumes from the approval node. On reject, it terminates with status
cancelled.
Approval state is persisted on the execution row, so a dashboard restart or browser refresh never loses the decision.
Validation and cost policies
Two gates stand between a draft and a successful publish:
Structural validation runs every time you save the canvas:
- Every required port must be wired.
- Variable references must match a declared variable.
- No cycles in the DAG (loops express iteration as a node).
- Every condition expression must parse.
Cost policy validation runs on Publish:
- Estimated worst-case cost per run must fit under the workflow’s
cost_limit. - The org’s per-day budget must have headroom (or
cost_policy: warn). - Approver lists for Approval nodes must resolve to real users.
Validation popover
The toolbar shows a validation badge — green when clean, amber for warnings, red with an error count when publish is blocked. Clicking the badge opens the validation issues popover anchored beneath it. The popover lists every issue with a severity icon and a per-issue Fix button:
- For node-scoped issues (missing config, unresolved variable, beta-node warning), Fix selects the offending node and scrolls it into view; the inspector switches to the relevant tab.
- For edge-scoped issues (unwired required port), Fix highlights the edge and pans to it.
- For workflow-scoped issues (cost ceiling, approver resolution), Fix focuses the corresponding setting in the right sidebar.
Errors block publish; warnings publish anyway and surface as a banner on the published workflow’s detail page. The popover is the recommended path to resolution — it’s faster than scanning the canvas by eye and consolidates errors, warnings, and beta-node notices in one place.
Tenant isolation
Every workflow carries an org_id and every execution is filtered by the caller’s resolved tenant. Concretely:
- Listing workflows only returns the caller’s org’s workflows.
- Loading a foreign workflow ID returns 404 (not 403 — we don’t leak existence).
- The webhook URL embeds the workflow ID, but the secret is per-org and the executor double-checks the
org_idbefore queuing the run. - The observability stats endpoint (
GET /api/v1/admin/workflows/stats) returns zeroed data for orgs with no executions, never another org’s roll-up.
This is enforced both at the service layer (workflow_service.py) and the route layer (admin_workflows.py); the regression test suite at tests/api/test_workflow_tenant_isolation.py covers every route that takes a workflow ID.
Debugging
When something goes wrong (and it will), the workflow builder gives you five progressively-detailed tools.
Live execution stream
Hit Run and the canvas overlays a live SSE stream onto each node. You see:
- Node transitions: queued → running → streaming → complete / failed.
- Token + cost deltas as they accumulate.
- The full prompt and response per LLM node (collapsible).
- True / False edge pills pulse teal as control flows through them.
Inspector Runs tab
Select any node and switch to the inspector’s Runs tab to see that node’s latest 10 executions across recent workflow runs. Each row expands to its raw event payload so you can diff a working run against a failing one without leaving the canvas. See Inspector for the full tab catalog.
Policy view
The inspector’s Policy tab shows the org governance that applies to the selected node (model allowlist, HITL thresholds, cost limits, PII / tool restrictions). When a run is blocked by policy, this is where you confirm which rule fired before opening the Governance Policies page to amend it.
Execution history
Every workflow has a History tab listing every run ever, with status, duration, cost, and triggered_by. Click any row to load the full execution into a read-only canvas view.
Breakpoints and step-through
In test mode, you can right-click any node and select Add breakpoint. The next test run pauses before executing that node and exposes the in-flight context as JSON in the inspector. Click Step over to advance one node at a time, or Continue to run to completion.
Time-travel debugging
If a real-mode run failed and you want to retry from a specific point, open the execution and click Replay from here on any node. The replay re-executes downstream nodes only, with the upstream output frozen at the original values. See Time-Travel Debugging for the full feature.
Import and Export
Workflows round-trip through JSON.
Export: open a workflow and click Settings → Export → Download JSON. The output is the canonical workflow definition — nodes, edges, settings, variables, but never secrets (the export strips webhook secrets and any vault references).
Import: from the /workflows index page, click Import and paste/upload a JSON file. Imported workflows always start as draft with a fresh id, regardless of what the JSON declared.
Example minimal export:
{
"name": "quickstart-classifier",
"description": "Classify support tickets",
"variables": [
{ "name": "ticket_text", "type": "string", "required": true }
],
"nodes": [
{
"id": "n1",
"type": "trigger",
"position": { "x": 0, "y": 0 },
"data": { "label": "Manual" }
},
{
"id": "n2",
"type": "llm_call",
"position": { "x": 250, "y": 0 },
"data": {
"label": "Classify",
"model": "claude-haiku-4-5",
"config": {
"system_prompt": "Classify as urgent/normal/spam.",
"user_prompt": "{{ticket_text}}"
}
}
},
{
"id": "n3",
"type": "output",
"position": { "x": 500, "y": 0 },
"data": { "label": "Verdict" }
}
],
"edges": [
{ "id": "e1", "source": "n1", "target": "n2" },
{ "id": "e2", "source": "n2", "target": "n3" }
]
}Limits
Soft limits intended to keep runs sane:
| Limit | Default | Notes |
|---|---|---|
| Nodes per workflow | 100 | Larger DAGs work but UI gets crowded; the visual builder is optimized for ≤ 50 |
| Edges per workflow | 200 | Same caveat as nodes |
| Variables per workflow | 50 | Mostly an authoring-clarity limit |
| Execution duration | 30 min | Configurable per workflow via settings.timeout |
| Concurrent runs per workflow | Tier-dependent | Free 1, Pro 10, Enterprise 100 |
| Cost ceiling per run | $1.00 default | Configurable per workflow via settings.cost_limit |
Hitting a limit produces a structured error in the execution event stream — never a silent truncation.
Observability
Every published workflow run writes to workflow_executions with status, duration_ms, total_cost, and org_id. The dashboard surfaces this in three places:
/workflows/{id}History tab — the full execution log for one workflow./system/ops→ Workflows tab — tenant-scoped roll-up of every workflow’s run health: failure rate, p50/p95 duration, total cost, top failing workflows. Switch the time window (24h / 7d / 30d) to scope the metrics./workflows/{id}/analytics— per-workflow drill-down with bottleneck analysis, path analytics, and funnel views.
Background: the system-ops Workflows panel hits GET /api/v1/admin/workflows/stats?window=24h|7d|30d. The endpoint is tenant-scoped, returns a zeroed payload for empty windows, and never leaks foreign-org executions — it’s the canonical place to wire alerts off of.
Palette stats footer
The node palette has a compact stats footer pinned at the bottom showing the same 24h roll-up at a glance: today’s runs, failure rate (tonal — good / warn / err based on < 5% / 5–15% / > 15%), p95 duration, and total cost. Auto-refreshes every 60 seconds while the builder is open. Clicking it deep-links to /system/ops?tab=workflows for the full breakdown. The footer is the fastest signal that “something is wrong out there” without leaving the builder.
API endpoints
All workflow APIs live under /api/v1/admin/workflows/* and require a valid JWT plus an admin role or enterprise tier.
| Method | Path | Purpose |
|---|---|---|
GET | /api/v1/admin/workflows | List workflows (paginated, tenant-scoped) |
POST | /api/v1/admin/workflows | Create a new workflow (starts in draft) |
GET | /api/v1/admin/workflows/{id} | Read one |
PUT | /api/v1/admin/workflows/{id} | Update (draft only) |
DELETE | /api/v1/admin/workflows/{id} | Soft delete |
POST | /api/v1/admin/workflows/{id}/publish | Freeze a version |
POST | /api/v1/admin/workflows/{id}/execute | Start a run |
GET | /api/v1/admin/workflows/{id}/executions/{exec_id}/stream | SSE stream of execution events |
POST | /api/v1/admin/workflows/{id}/executions/{exec_id}/cancel | Cancel an in-flight run |
POST | /api/v1/admin/workflows/{id}/executions/{exec_id}/approve | Approve a paused run |
GET | /api/v1/admin/workflows/stats | Tenant-scoped run health roll-up (24h/7d/30d) |
Full OpenAPI is published at https://api.curate-me.ai/openapi.json and viewable at /docs when running locally.
Mobile and tablet
The builder is desktop-first but degrades cleanly down to tablet and phone widths. The shell switches structural variants at three breakpoints — same content, different layout:
| Width | Mode | What changes |
|---|---|---|
≥ 1024px | desktop | Default Wave 13 shell — palette as a left rail, inspector as a right rail, full toolbar. |
768–1023px | tablet | Palette collapses to a 56px icon rail with a sheet drawer on tap. Inspector stays as a right rail. Toolbar collapses secondary actions into a More dropdown. |
640–767px | mobile | Same palette behavior as tablet. The inspector becomes a bottom sheet with peek + expanded states and a drag handle. Toolbar reduces to identity + Run. |
< 640px | read-only | Canvas locks to read-only. A banner explains “Open the builder on tablet or larger to edit.” Viewing, pan, zoom, and the execution stream still work. |
The hook backing this lives at apps/dashboard/components/workflow-builder/useResponsiveBuilder.ts and is the single source of truth for the breakpoints — toolbar, inspector, palette, and canvas all read from it.
Caveats:
- Drag-drop from the palette is unsupported below 1024px; on tablet/mobile, tap-to-add is used instead (the palette drawer’s node entries call the same
onAddNodehandler as drag). - Multi-select and group operations are desktop-only.
- The 6-tab inspector is fully available on tablet (right rail) and mobile (bottom sheet, expanded state) — every tab works at every width.
Screenshot inventory
Production screenshots will be generated once visual-regression baselines are re-recorded post-implementation. Until then, the canonical visual references live in the Phase 2 Claude Design handoff bundle at apps/dashboard/design/workflow-builder-phase-2-handoff/:
| Surface | Reference file |
|---|---|
| Empty-state poster | empty-state.jsx |
| Mid-execution state (running workflow, status pills, inspector Runs tab) | mid-execution.jsx |
| Validation popover + Fix affordances | validation-state.jsx |
| Mobile / tablet variants (palette drawer, inspector bottom sheet, toolbar More) | responsive.jsx |
| Palette stats footer | palette-detail.jsx |
| Composed prototype (all 5 scenes) | Workflow Builder.html |
The handoff bundle is design reference only — HTML/CSS/JS prototypes, not production code. The production implementation in apps/dashboard/components/workflow-builder/ reproduces the visual intent using Orbital tokens. See the bundle’s README.md for context on the design medium.
Where to go next
- Approval Queues — runtime mechanics of the Approval node.
- Cost Tracking — how per-run cost flows into budgets and anomaly detection.
- Time-Travel Debugging — replay execution from any node.
- Governance Policies — org-level guardrails that apply to every workflow run.