This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/docs/ICD/workflows.md
Jeffrey Smith bf8082e69f Changeset 0.35.0 (#209)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
2026-03-20 09:59:53 +00:00

20 KiB

Workflows

Team-owned, stage-based process execution. The channel is the runtime — personas drive each stage, and the existing tool/notes infrastructure handles structured data collection.

Definitions

List Workflows

GET /workflows?team_id=...

Returns { "data": [...] }. If team_id is provided, scoped to that team. Otherwise returns global (team_id IS NULL) workflows.

Auth: Authenticated.

Create Workflow

POST /workflows
{
  "name": "Customer Intake",
  "slug": "customer-intake",
  "description": "Collect customer info and route to support",
  "entry_mode": "public_link",
  "team_id": "uuid|null",
  "branding": { "accent_color": "#2563eb", "tagline": "Welcome" },
  "retention": { "mode": "archive", "delete_after_days": 90 }
}

slug auto-generated from name if omitted. Must be 2-64 chars, lowercase alphanumeric and hyphens. Unique within scope (team or global).

entry_mode: public_link (visitors can start via URL) or team_only (only team members can start instances).

Auth: workflow.create permission required.

Response: 201 with workflow object.

Get Workflow

GET /workflows/:id

Returns workflow with its stages included in the stages array.

Auth: Authenticated.

Update Workflow

PATCH /workflows/:id

Partial update. Accepted fields: name, description, branding, entry_mode, is_active, on_complete, retention, webhook_url.

Any edit increments the version number.

Auth: workflow.create permission required.

Delete Workflow

DELETE /workflows/:id

Cascades: deletes all stages, versions, and active instances.

Auth: workflow.create permission required.

Workflow Object

{
  "id": "uuid",
  "team_id": "uuid|null",
  "name": "Customer Intake",
  "slug": "customer-intake",
  "description": "...",
  "branding": { "accent_color": "#2563eb", "logo_url": "...", "tagline": "..." },
  "entry_mode": "public_link|team_only",
  "is_active": true,
  "version": 2,
  "on_complete": { "action": "start_workflow", "target_slug": "follow-up", "data_map": {"name": "customer_name"} },
  "retention": { "mode": "archive|delete", "delete_after_days": 90 },
  "webhook_url": "https://...",
  "webhook_secret": "...",
  "created_by": "uuid",
  "created_at": "...",
  "updated_at": "...",
  "stages": [...]
}

Stages

Ordered steps within a workflow. Each stage has a driving persona, optional human assignment team, and a stage mode that controls how data is collected.

Stage Modes (v0.29.3)

Mode Description
chat_only Default. Persona drives the conversation.
form_only UI-rendered form, no LLM. Visitor fills out fields.
form_chat Both form and chat available. Persona assists alongside the form.
review Human review queue. Shows structured stage_data with approve/reject.

List Stages

GET /workflows/:id/stages

Returns { "data": [...] } ordered by ordinal.

Auth: Authenticated.

Create Stage

POST /workflows/:id/stages
{
  "name": "Collect Info",
  "ordinal": 0,
  "persona_id": "uuid|null",
  "assignment_team_id": "uuid|null",
  "stage_mode": "chat_only|form_only|form_chat|review",
  "form_template": { "fields": [...] },
  "history_mode": "full|summary|fresh",
  "auto_transition": false,
  "transition_rules": { "auto_assign": "round_robin", "conditions": [...] },
  "sla_seconds": 3600
}

stage_mode: controls data collection method. Default: chat_only. form_only and form_chat require a typed form_template with fields.

history_mode: what chat history the next stage sees. full = complete, summary = utility-role summary, fresh = clean slate.

form_template: when stage_mode is form_only or form_chat, the template uses a typed schema (see below). For chat_only stages, legacy freeform templates are still supported as guidance for the LLM.

transition_rules.auto_assign: round_robin assigns to team members in rotation. Null = unassigned (manual claim).

transition_rules.conditions: array of routing conditions (v0.35.0). See Conditional Routing.

sla_seconds: optional SLA budget in seconds for this stage. Used by the monitoring dashboard for deadline tracking. Null = no SLA.

Auth: workflow.create permission required.

Typed Form Template (v0.29.3)

{
  "fields": [
    { "key": "name", "type": "text", "label": "Full Name", "required": true, "validation": { "min_length": 2 } },
    { "key": "email", "type": "email", "label": "Email", "required": true },
    { "key": "dept", "type": "select", "label": "Department", "options": [
      { "value": "eng", "label": "Engineering" },
      { "value": "sales", "label": "Sales" }
    ]},
    { "key": "notes", "type": "textarea", "label": "Additional Notes",
      "condition": { "when": "dept", "op": "eq", "value": "eng" } }
  ],
  "hooks": {
    "package_id": "uuid",
    "validate": "on_validate",
    "on_submit": "on_form_submit"
  }
}

Field types: text, email, number, date, textarea, select, checkbox, file.

Validation rules (in validation object per field):

  • min_length, max_length — string length bounds
  • pattern — regex pattern for text/email fields
  • min, max — numeric bounds for number fields
  • min_date, max_date — date range bounds (YYYY-MM-DD format)

Conditional fields (v0.35.0): A field can have a condition object that controls visibility. When the condition is not met, the field is hidden on the client and skipped during server-side validation.

{ "when": "field_key", "op": "eq|neq|in|exists", "value": "..." }
  • eq (default): field is shown when the referenced field equals value
  • neq: shown when not equal
  • in: shown when the referenced field's value is in the value array
  • exists: shown when the referenced field has any non-empty value

Progressive forms / fieldsets (v0.35.0): For multi-step forms within a single stage, use fieldsets instead of (or alongside) fields:

{
  "fieldsets": [
    { "label": "Contact Info", "fields": [
      { "key": "name", "type": "text", "label": "Name", "required": true },
      { "key": "email", "type": "email", "label": "Email", "required": true }
    ]},
    { "label": "Details", "fields": [
      { "key": "description", "type": "textarea", "label": "Description" }
    ]}
  ]
}

When fieldsets is present, the visitor sees one fieldset at a time with next/back navigation. All fields from all fieldsets are submitted together. On the server, ParseTypedFormTemplate flattens fieldsets into the fields array for validation — backward compatible with existing logic.

Hooks: optional Starlark hooks via extension packages.

  • validate: called before form submission is accepted; can return field errors
  • on_submit: fire-and-forget hook after successful submission

Requires forms.validate extension permission.

Update Stage

PUT /workflows/:id/stages/:sid

Full replacement of stage fields.

Auth: workflow.create permission required.

Delete Stage

DELETE /workflows/:id/stages/:sid

Auth: workflow.create permission required.

Reorder Stages

PATCH /workflows/:id/stages/reorder
{ "ordered_ids": ["stage-uuid-2", "stage-uuid-1", "stage-uuid-3"] }

Sets ordinals based on array position.

Auth: workflow.create permission required.

Stage Object

{
  "id": "uuid",
  "workflow_id": "uuid",
  "ordinal": 0,
  "name": "Collect Info",
  "stage_mode": "chat_only",
  "persona_id": "uuid|null",
  "assignment_team_id": "uuid|null",
  "form_template": {},
  "history_mode": "full",
  "auto_transition": false,
  "transition_rules": {},
  "surface_pkg_id": "uuid|null",
  "sla_seconds": 3600,
  "created_at": "..."
}

Versions (Immutable Snapshots)

Publish

POST /workflows/:id/publish

Creates an immutable snapshot of the current workflow definition, stages, and persona tool grants. Running instances are pinned to their version.

Returns 201 with the version object. Returns 409 if the current version number has already been published (edit the workflow to increment first).

Auth: workflow.create permission required.

Get Version

GET /workflows/:id/versions/:version

Returns the version snapshot.

Auth: Authenticated.

Version Object

{
  "id": "uuid",
  "workflow_id": "uuid",
  "version_number": 2,
  "snapshot": { "workflow": {...}, "stages": [...] },
  "created_at": "..."
}

Instances

A workflow instance is a channel with type=workflow. The channel's workflow_id, workflow_version, current_stage, stage_data, and workflow_status columns track instance state.

Start Instance

POST /workflows/:id/start

Creates a new workflow channel, sets current_stage=0, pins the latest published version. Returns the channel object.

Auth: Authenticated (for team members). Visitors use the entry API.

Get Instance Status

GET /channels/:id/workflow/status
{
  "workflow_id": "uuid",
  "workflow_version": 2,
  "current_stage": 1,
  "stage_data": { "name": "Jane", "email": "jane@co.com" },
  "status": "active|completed|stale",
  "last_activity_at": "...",
  "stage_entered_at": "..."
}

Auth: Authenticated.

Advance Stage

POST /channels/:id/workflow/advance
{ "data": { "name": "Jane", "email": "jane@co.com" } }

Merges data into the channel's accumulated stage data. The next stage is determined by the conditional routing engine (v0.35.0): transition_rules.conditions are evaluated against the merged stage data. First matching condition selects the target stage; if no conditions match or none are defined, advances to ordinal + 1. If the target is beyond the last stage, marks the workflow as completed and sets ai_mode=off.

After a successful transition, the on_advance hook fires if configured (see below). On completion, triggers on_complete chaining and webhook delivery if configured.

Auth: Authenticated.

Reject (Go Back)

POST /channels/:id/workflow/reject
{ "reason": "Missing required field: email" }

Moves back one stage. Cannot go below stage 0. reason is required — persisted as a system message in the channel history.

Auth: Authenticated.

Workflow Advance Tool

The workflow_advance tool is available to personas during workflow completions. When the LLM determines enough data has been collected, it calls this tool to trigger the stage transition programmatically. The tool has a RequireWorkflow predicate — it only appears in workflow channels.

Assignments

Human review queue for workflow stages that have an assignment_team_id.

List My Assignments

GET /workflow-assignments/mine

Returns assignments where assigned_to is the current user or status=unassigned for the user's teams.

Auth: Authenticated.

Claim Assignment

POST /workflow-assignments/:id/claim

Sets assigned_to to the current user, status to claimed. Returns 409 if the assignment is already claimed or not found.

Auth: Authenticated.

Complete Assignment

POST /workflow-assignments/:id/complete

Marks assignment as completed. Does NOT auto-advance the stage — the human reviews and manually advances or the persona tool does it. Returns 409 if the assignment is not in claimed state.

Auth: Authenticated.

List Team Assignments

GET /teams/:teamId/assignments?status=unassigned

Assignments for the team, filtered by status (default: unassigned). Valid values: unassigned, claimed, completed.

Auth: Authenticated (team member).

Get Assignment (v0.35.0)

GET /workflow-assignments/:id

Returns the full assignment including review comments.

Auth: Authenticated.

Add Review Comment (v0.35.0)

POST /workflow-assignments/:id/comment
{ "text": "Looks good, approved with minor note about address." }

Appends a comment to the assignment's review_comments array. Comments are visible to the next stage when history_mode=full.

Auth: Authenticated.

Assignment Object

{
  "id": "uuid",
  "channel_id": "uuid",
  "stage": 1,
  "team_id": "uuid",
  "assigned_to": "uuid|null",
  "status": "unassigned|claimed|completed",
  "review_comments": [
    { "text": "...", "user_id": "uuid", "created_at": "..." }
  ],
  "created_at": "...",
  "claimed_at": "...|null",
  "completed_at": "...|null"
}

Visitor Entry

Public-facing workflow entry for unauthenticated visitors.

Landing Page

GET /w/:id
GET /w/:id/:slug

Server-rendered branded page. Shows workflow name, tagline, branding colors, and a "Start" button. If the visitor has an existing session cookie for this workflow, resumes the conversation.

Not an API endpoint — returns HTML.

Start Visitor Session

POST /api/v1/workflow-entry/:scope/:slug

No request body. Display name is auto-generated (Visitor, Visitor 2, etc.).

:scope is a team ID or global. Creates a session participant, creates a workflow channel, sets allow_anonymous=true. Returns:

{
  "channel_id": "uuid",
  "session_id": "uuid",
  "redirect_to": "/w/channel-uuid"
}

The session token is set as a cookie (sb_session) for subsequent requests. redirect_to is the workflow chat surface URL.

Visitor Message/Completion

Visitors interact through the workflow channel using session-scoped routes:

POST /api/v1/w/:id/messages
GET  /api/v1/w/:id/messages
POST /api/v1/w/:id/completions

These use AuthOrSession middleware — the session cookie identifies the visitor without requiring a JWT.

Form Submission (v0.29.3)

For stages with stage_mode of form_only or form_chat, visitors submit structured form data through dedicated endpoints.

Get Form Template

GET /api/v1/w/:id/form

Returns the current stage's typed form template and submission status.

{
  "stage_mode": "form_only",
  "stage_name": "Contact Info",
  "form_template": { "fields": [...] },
  "submitted": false
}

Auth: AuthOrSession (JWT or session cookie).

Submit Form

POST /api/v1/w/:id/form-submit
{
  "name": "Jane Doe",
  "email": "jane@example.com",
  "dept": "eng"
}

Validates data against the typed form template. Returns field-level errors on validation failure:

{
  "error": "validation failed",
  "errors": [
    { "key": "email", "message": "invalid email format" },
    { "key": "name", "message": "required field missing" }
  ]
}

On success, merges data into stage_data. For form_only stages with auto_transition=true, auto-advances to the next stage.

Emits workflow.form_submitted WebSocket event on success.

Auth: AuthOrSession (JWT or session cookie).

Staleness Sweep

Background goroutine. Checks active workflow instances for staleness (no activity within WORKFLOW_STALE_HOURS, default 72). Stale instances are marked workflow_status=stale. Retention policy then applies: archive keeps the channel, delete removes it after delete_after_days.

On-Complete Chaining

When a workflow completes and has on_complete configured:

{
  "on_complete": {
    "action": "start_workflow",
    "target_slug": "follow-up-survey",
    "data_map": { "name": "customer_name", "case_id": "case_id" }
  }
}

action must be start_workflow. target_slug identifies the workflow to chain into (looked up in the same team scope). data_map is a flat key remapping from source stage data keys to target initial data keys. If data_map is empty/omitted, all stage data is passed through as-is.

The system starts a new instance of the target workflow, passing mapped stage data as initial context. Webhook delivery (if configured) fires before chaining.

Conditional Routing (v0.35)

The routing engine evaluates transition_rules.conditions on the departing stage against accumulated stage_data. Conditions are evaluated in order; first match wins.

Condition Object

{
  "conditions": [
    { "field": "category", "op": "eq", "value": "billing", "target_stage": "Billing Review" },
    { "field": "priority", "op": "gte", "value": 5, "target_stage": "Urgent" }
  ]
}
Field Type Description
field string Key in stage_data to evaluate
op string Operator (see below)
value any Value to compare against
target_stage string Stage name or ordinal to route to

Operators: eq, neq, gt, lt, gte, lte, in, contains, exists, not_exists.

target_stage resolves by name (case-insensitive) first, then as a numeric ordinal. Backward jumps are allowed (loop stages).

If no condition matches, falls back to ordinal + 1.

workflow_route Tool (v0.35.0)

Persona-callable tool for AI-triggered routing. Available only in workflow channels (RequireWorkflow predicate).

{
  "name": "workflow_route",
  "parameters": {
    "target_stage": "Stage name to route to",
    "reason": "Explanation for the routing decision"
  }
}

Resolves target_stage by name. Records the routing decision in stage_data._route_history_latest with from, to, reason, ts.

Starlark workflow.route()

workflow.route(channel_id, target_stage, reason)

Extension-callable routing for programmatic escalation patterns. Requires workflow.access permission.

on_advance Hook (v0.35.0)

Starlark entry point that fires synchronously after a stage transition. Configured per-stage in transition_rules:

{
  "on_advance": {
    "package_id": "uuid",
    "entry_point": "on_advance"
  }
}

The hook receives a context dict:

def on_advance(ctx):
    # ctx["channel_id"]     - workflow channel ID
    # ctx["previous_stage"] - ordinal of the departed stage
    # ctx["current_stage"]  - ordinal of the new stage
    # ctx["stage_data"]     - accumulated stage data dict
    return {"stage_data": ctx["stage_data"]}  # enriched
    # return None  — no change
    # return {"error": "msg"}  — reject (logged, not rolled back)

Use cases: external API enrichment via http.fetch, cross-field validation, data transformation between stages.

Monitoring Dashboard (v0.35.0)

Admin endpoints for tracking active workflow instances and SLA status.

List Active Instances

GET /admin/workflows/monitor/instances
GET /teams/:teamId/workflows/monitor/instances

Returns all active (workflow_status=active) instances with SLA info.

{
  "data": [{
    "channel_id": "uuid",
    "channel_title": "...",
    "workflow_id": "uuid",
    "workflow_name": "...",
    "current_stage": 1,
    "stage_name": "Review",
    "age_seconds": 3600,
    "stage_age_seconds": 1200,
    "sla_seconds": 7200,
    "sla_remaining_seconds": 6000,
    "sla_breached": false,
    "last_activity_at": "..."
  }]
}

SLA is computed as stage_entered_at + sla_seconds vs current time.

Auth: Admin (global) or team member (team-scoped).

Stage Funnel

GET /admin/workflows/monitor/funnel/:id

Returns per-stage instance counts for bottleneck detection.

{
  "data": [
    { "stage_ordinal": 0, "stage_name": "Intake", "count": 5 },
    { "stage_ordinal": 1, "stage_name": "Review", "count": 12 },
    { "stage_ordinal": 2, "stage_name": "Complete", "count": 2 }
  ]
}

Auth: Admin.

Stale Instances

GET /admin/workflows/monitor/stale?threshold_hours=48

Returns instances where last_activity_at exceeds the threshold. Default threshold: 48 hours.

Auth: Admin.

WebSocket Events

Event When
workflow.advanced Stage transition (includes new stage info)
workflow.completed Workflow finished
workflow.assigned New assignment created
workflow.claimed Assignment claimed by user
workflow.form_submitted Form data submitted for a form stage