Skip to content

Workflow

New in 1.3.0

Workflow orchestration is available from v1.3.0. Earlier releases (v1.2.0 and below) do not ship this capability.

What workflow is: a way to compose multiple digital employees plus system actions (approval / channel dispatch / memory write) into a linear-step business process. Each step can be gated by the previous step's output, fan out in parallel, wait for human approval, or persist results into an employee's MEMORY.md.

What workflow is not:

  • Not a replacement for ReAct / Plan-and-Execute — single-agent multi-turn reasoning still lives in those engines
  • Not a low-code drag-and-drop if/else builder — v0 is JSON-first (canvas comes in v1)
  • Not a 30-node Dify-style orchestrator — MateClaw workflows stay deliberately minimal: a linear array of steps with one mode field expressing the control flow

v1.3.0 scope

v0 = internal alpha. 7 step modes + 6 trigger pattern types. loop and invoke_skill are deferred. Run it on a flagship account / internal workspace before rolling out broadly.


One-minute overview

json
{
  "schemaVersion": "1.0",
  "inputs": [
    { "name": "customer", "type": "json" }
  ],
  "steps": [
    {
      "name": "enrich",
      "agentName": "data-analyst",
      "promptTemplate": "Enrich and return strict JSON: {{ inputs.customer | toJson }}",
      "mode": { "type": "sequential" },
      "outputVar": "enriched",
      "outputContentType": "json"
    },
    {
      "name": "vip-route",
      "agentName": "enterprise-sales",
      "promptTemplate": "VIP onboarding for {{ outputs.enriched.name }}",
      "mode": {
        "type": "conditional",
        "expression": "{{ outputs.enriched.tier == 'enterprise' }}"
      }
    },
    {
      "name": "notify-feishu",
      "agentName": "ops-bot",
      "promptTemplate": "Notify feishu: {{ outputs.enriched }}",
      "mode": { "type": "fan_out" }
    },
    {
      "name": "notify-email",
      "agentName": "ops-bot",
      "promptTemplate": "Notify email: {{ outputs.enriched }}",
      "mode": { "type": "fan_out" }
    },
    {
      "name": "wait-acks",
      "mode": { "type": "collect" }
    },
    {
      "name": "record",
      "promptTemplate": "Onboarded {{ inputs.customer.name }}",
      "mode": {
        "type": "write_memory",
        "employeeId": "{{ outputs.enriched.assignedEmployeeId }}",
        "file": "MEMORY.md",
        "mergeStrategy": "append"
      }
    }
  ]
}

How it reads:

  1. enrich asks the data analyst to structure the customer info as JSON
  2. If tier == enterprise, route to the enterprise-sales employee for VIP onboarding
  3. In parallel (fan_out), notify Feishu and notify email
  4. collect waits for both notifications
  5. Append the result to the employee's MEMORY.md

Core concepts

Seven step modes (v1.3.0)

ModeBehaviorRequired fieldsKey semantics
sequentialRun after the previous step; previous output → Default mode
fan_outRuns in parallel with consecutive fan_out steps; all receive the same Boundary detected at compile time: from this step onward, the first non-fan_out / non-collect step terminates the group
collectJoins the most recent fan_out group's outputs with \n\n---\n\n into At least 2 consecutive fan_out steps must precede; compile-time check
conditionalRuns only if the Pebble expression is trueexpressionWhen false, skipped; is preserved (carries over previous step's)
await_approvalPauses the run; sends an approvalapprovalKind, approverChannels[]Resumes to next step on approval; timeout follows workspace policy
dispatch_channelMulti-channel delivery of channels[]Per-channel failure follows errorMode
write_memoryWrites employee memory fileemployeeId, file, mergeStrategyFour strategies: append / replace_section / upsert_kv / overwrite

Not in v1.3.0: loop (iterate N times or per-item over an array) and invoke_skill (call a skill without going through an employee). Coming based on user feedback.

Expressions: a Pebble subset

Workflow does not use a full template engine — it supports the same Pebble subset as Kestra, just enough to gate conditionals and reference variables, with no code execution.

CategorySyntax
Variable referencesinputs.X / outputs.varname.field / vars.X / now / flow.id
Operators== != < <= > >= and or not + -
Built-in filterslength / lower / upper / default('x') / toJson / fromJson / date(format)
JSONPath| jq('.field.subfield')
String tests| contains('x') / | startsWith('x') / | matches('regex')

Not supported (rejected at compile time):

  • User-defined functions / macros
  • include / extends
  • File I/O / network I/O
  • Any side-effecting operations

Output type: text vs json

Each step's outputContentType decides how downstream steps can access it:

outputContentTypeDefaultPebble access rules
textoutputs.X is a string; outputs.X.field fails at compile time; | jq(...) fails at runtime
jsonRuntime JSON.parse; failure follows errorMode; field access / jq(...) are valid

Agent steps default to outputContentType=text — LLM natural-language output isn't structured JSON. To do conditionals or field access, you must:

  1. Explicitly request strict JSON in the promptTemplate ("return strict JSON: {...}")
  2. Set that step's outputContentType to json

Compile-time illegal combinations (publish rejects)

CombinationReason
Multiple consecutive fan_out with no collect to terminate for the next step is ambiguous
collect without preceding fan_outNothing to collect
await_approval mixed inside a fan_out groupMultiple concurrent approvals fired with no aggregation UX
agentName references a non-existent / disabled / cross-workspace employeeACL fail
Pebble expression references an undeclared variableCompile-time
outputs.X.field but step X is textCompile-time type error
dispatch_channel references a channel not in the workspace allowlistACL
write_memory references an employeeId outside the workspaceACL
Step count > 200 (default cap)Runaway-config guard

Publish runs WorkflowCompiler.validate(graphJson) → List<CompileError>. Each error points at a step name + field path; the Monaco editor highlights them inline.


Using workflow from the UI

Entry point

Workflows (sidebar) → list → + New.

TIP

The Workflows list is empty on a fresh install. That's intentional — v0 ships no built-in templates; flagship accounts co-author them.

Editor (v1.3.0 = JSON only)

  • Monaco editor: JSON-schema validation, autocomplete, static Pebble checking
  • Template dropdown: built-in skeletons fetched from GET /api/v1/workflows/draft/templates
  • Pre-compile: POST /api/v1/workflows/{id}/compile returns compile diagnostics — does not write a revision, does not actually run
  • Publish: compile → ACL validate → write a new mate_workflow_revision row (integer revision +1)

Canvas comes in v1

The @vue-flow/core canvas has a UI shell in v1.3.0, but it renders the step array as a node chain — not drag-to-edit. Double-click a node to open its field form; the primary edit path is still JSON. Full visual editing lands in v1.4+.

Natural language → workflow draft (v1.3.0)

POST /api/v1/workflows/draft/generate takes a free-form description ("I want a customer ticket triage flow with a Feishu entry, routing by tier — enterprise / pro / standard — to different handlers"), runs an internal agent to emit the corresponding graph_json, and immediately compiles + returns with diagnostics attached.

Use cases:

  • Authors who don't know the JSON DSL get a publishable first draft to refine in Monaco
  • Bulk-feeding old SOP docs through the generator to get candidate workflow templates
  • During customer co-creation, turn "how I want this to work" into something visualizable fast

Response shape:

json
{
  "graphJson": "...",          // can be PUT directly into a draft
  "compileErrors": [...],      // same diagnostics as /compile
  "modelUsed": "qwen-plus",
  "tokenUsage": { ... }
}

Doesn't replace Monaco editing

The generator never publishes directly — it only emits a draft (via saveDraft); a human still has to review → compile → publish. The generated JSON may carry compile errors; the author cleans them up before publishing.

Run history

Every run persists as mate_workflow_run + mate_workflow_run_step. Detail view shows:

  • Per-step input / output (payload URI references)
  • Per-step duration + token usage
  • Cross-step failure chain highlight
  • For paused await_approval steps: who's approving, how long it's been waiting

Trigger sources

A workflow run can only start through Triggers or via await_approval resume — v0 has no "fire one now" endpoint. See API reference above for details.

1.4.0: triggers now live in the Scheduler

As of v1.4.0, Scheduled Jobs and Triggers are merged into a single Scheduler page (Settings → Scheduler, route /settings/scheduler) with three tabs: Scheduled Jobs / Event Triggers / Run History. To attach a trigger to a workflow, create a target_type=workflow rule on the Scheduler's Event Triggers tab. See Triggers.


API reference

All endpoints live under /api/v1/workflows/. Requests must carry the X-Workspace-Id header.

CRUD

MethodPathDescription
GET/api/v1/workflowsList all workflows in the current workspace
POST/api/v1/workflowsCreate a new workflow (draft starts empty)
GET/api/v1/workflows/{id}Fetch workflow metadata + inline draft
PUT/api/v1/workflows/{id}Update workflow metadata (name / description / enabled)
PUT/api/v1/workflows/{id}/draftSave the inline draft graph_json (does not compile)
DELETE/api/v1/workflows/{id}Soft-delete

Compile / Publish

MethodPathDescription
POST/api/v1/workflows/{id}/compileCompile the current draft and return diagnostics — does not persist a revision
POST/api/v1/workflows/{id}/publishCompile + persist a new revision; updates latest_revision_id

Draft generator (built-in in v1.3.0)

MethodPathDescription
GET/api/v1/workflows/draft/templatesList built-in draft templates
POST/api/v1/workflows/draft/preview-compileCompile arbitrary graph_json — surfaces real diagnostics before a workflow row exists
POST/api/v1/workflows/draft/generateNatural language → workflow draft — describe the flow, an agent emits graph_json + compile diagnostics

Run inspection / resume

MethodPathDescription
GET/api/v1/workflows/{id}/runs?limit=...Recent runs of a workflow (default 50)
GET/api/v1/workflows/runs/paused?limit=...All paused runs across the workspace (operator entry point)
GET/api/v1/workflows/runs/{runId}One run's detail + all step rows (input / output / duration)
POST/api/v1/workflows/runs/{runId}/resumeResume from await_approval pause (called automatically when an approval lands; not for manual use)

v0 has no standalone "start run" endpoint

There are only two paths to actually start a workflow run:

  1. Via a trigger — configure a trigger in Triggers pointing at this workflow (target_type=workflow); when an event arrives the engine starts the run
  2. Via await_approval resume — the resume endpoint pushes a paused run forward

There is no POST /api/v1/workflows/{id}/runs "fire one now" endpoint in v0. For a dry run, use /draft/preview-compile to get compile output (compile only — no persist, no real run), or attach a temporary webhook trigger. A manual run-start endpoint is on the RFC but lands in a later release.


Security model

Three-layer ACL

RoleCapabilities
workflow:authorEdit drafts, read own runs
workflow:publisherPublish revisions; static ACL checks fire here
workflow:operatorStart/stop triggers, cancel runs, view other people's runs

Per-step execution identity

Every step carries in its ExecutionContext:

  • workspaceId: must equal the workflow's workspace
  • actingAgentId: for sequential and the three MateClaw modes → that step's agent; for other modes → publisher as fallback
  • triggeredBy / workflowId / revisionId / runId: for audit traceability

Cross-workspace isolation

At publish time WorkflowAclValidator.checkAll(graphJson) runs:

  • agentName references must point to an employee in the current workspace
  • dispatch_channel channels must be in the workspace allowlist
  • write_memory employeeIds must be inside the current workspace

Any failure → publish fails, transaction rolls back, no revision row written, no latest_revision_id update.

Relationship with MCP per-agent tool binding

Workflow cannot grant employees additional tools. When an agent step calls a tool, it goes through the same AgentBindingService.getEffectiveToolNames(agentId) ACL — what an employee can do inside a workflow is exactly what it can do in normal chat.


Internal storage URI for payloads

Workflow inputs / outputs / intermediate artifacts above the 4KB default threshold are auto-spilled to the mate_workflow_payload table (v1.3.0: same-DB storage) or local filesystem fallback, and replaced inline with a payload:// URI. This avoids large contexts blowing out the message column — see commit 9c81dba0 feat(workflow): payload fs fallback for medium-size payloads.

text
payload://run/abc123/step/enrich/output → resolved by the backend at access time

The UI lazy-loads on demand.


Data model

The workflow subsystem touches 8 tables:

TablePurpose
mate_workflowWorkflow root (id / name / workspace)
mate_workflow_revisionPublished revisions (integer revision; full graph_json snapshot; immutable)
mate_workflow_runOne execution (runId / triggerSource / status / startedAt / endedAt)
mate_workflow_run_stepPer-step input/output/duration inside a run
mate_workflow_run_pausePersistent await_approval pause state (survives restart)
mate_workflow_payloadLarge-payload internal storage (target for payload:// URI)
mate_triggerTrigger configurations (with cron pattern_version)
mate_trigger_eventEvent dedup + rate-limit history

Known limitations (v1.3.0)

  • No drag-to-edit canvas — the canvas is read-only chain rendering; primary edit path is JSON
  • No loop step — can't iterate per-item or retry N times. Workaround: a fixed number of fan_out branches, or higher-level scheduling of multiple runs
  • No invoke_skill step — skills must be attached to an agent and invoked through the agent
  • No cross-workspace sharing — to reuse a workflow template across workspaces, copy it
  • No realtime collaborative editing — concurrent edits to the same draft: last write wins
  • No per-step retry policyerrorMode.retry is step-wide; finer-grained retry is deferred