# 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
```

```json
{
  "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

```json
{
  "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
```

```json
{
  "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](#conditional-routing-v035).

`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)

```json
{
  "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.

```json
{ "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`:

```json
{
  "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
```

```json
{ "ordered_ids": ["stage-uuid-2", "stage-uuid-1", "stage-uuid-3"] }
```

Sets ordinals based on array position.

**Auth:** `workflow.create` permission required.

### Stage Object

```json
{
  "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

```json
{
  "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
```

```json
{
  "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
```

```json
{ "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
```

```json
{ "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
```

```json
{ "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

```json
{
  "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:

```json
{
  "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.

```json
{
  "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
```

```json
{
  "name": "Jane Doe",
  "email": "jane@example.com",
  "dept": "eng"
}
```

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

```json
{
  "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:

```json
{
  "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

```json
{
  "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).

```json
{
  "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()

```python
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`:

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

The hook receives a context dict:

```python
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.

```json
{
  "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.

```json
{
  "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 |
