Changeset 0.30.2 cs2 (#202)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit is contained in:
330
docs/WORKFLOW-PACKAGES.md
Normal file
330
docs/WORKFLOW-PACKAGES.md
Normal file
@@ -0,0 +1,330 @@
|
||||
# Workflow Packages
|
||||
|
||||
> v0.30.2
|
||||
|
||||
Workflow packages bundle a workflow definition, stage configuration, and
|
||||
optional custom surfaces into a single `.pkg` file. They can be exported
|
||||
from one instance and imported into another.
|
||||
|
||||
## Concepts
|
||||
|
||||
A **workflow** is a multi-stage process (intake form → AI chat → human
|
||||
review → completion). Each stage has a **surface** that renders the UI:
|
||||
built-in surfaces (form, chat, review) or a custom package surface via
|
||||
`surface_pkg_id`.
|
||||
|
||||
## Exporting a Workflow
|
||||
|
||||
```
|
||||
GET /api/v1/admin/workflows/:id/export
|
||||
```
|
||||
|
||||
Returns a `.pkg` ZIP with:
|
||||
|
||||
```
|
||||
manifest.json workflow manifest (type: "workflow")
|
||||
js/ surface assets (if the workflow references package surfaces)
|
||||
script.star optional Starlark handlers
|
||||
```
|
||||
|
||||
The manifest includes the full workflow definition and all stages:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "onboarding-flow",
|
||||
"title": "Employee Onboarding",
|
||||
"type": "workflow",
|
||||
"version": "1.0.0",
|
||||
"workflow_definition": {
|
||||
"name": "Employee Onboarding",
|
||||
"slug": "employee-onboarding",
|
||||
"description": "New hire onboarding workflow",
|
||||
"entry_mode": "public_link",
|
||||
"stages": [
|
||||
{
|
||||
"name": "Intake",
|
||||
"ordinal": 0,
|
||||
"stage_mode": "form_only",
|
||||
"history_mode": "full",
|
||||
"auto_transition": false,
|
||||
"form_template": {
|
||||
"fields": [
|
||||
{ "key": "full_name", "type": "text", "label": "Full Name", "required": true },
|
||||
{ "key": "email", "type": "email", "label": "Email", "required": true },
|
||||
{ "key": "department", "type": "select", "label": "Department",
|
||||
"options": ["Engineering", "Sales", "Marketing"] }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "AI Orientation",
|
||||
"ordinal": 1,
|
||||
"stage_mode": "chat_only",
|
||||
"history_mode": "full",
|
||||
"persona_id": null
|
||||
},
|
||||
{
|
||||
"name": "Manager Review",
|
||||
"ordinal": 2,
|
||||
"stage_mode": "review",
|
||||
"history_mode": "summary",
|
||||
"surface_pkg_id": "review-dashboard"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Importing a Workflow
|
||||
|
||||
```
|
||||
POST /api/v1/admin/packages/install
|
||||
Content-Type: multipart/form-data
|
||||
file=@onboarding-flow.pkg
|
||||
```
|
||||
|
||||
The install handler detects `type: "workflow"` and:
|
||||
|
||||
1. Creates the workflow from `workflow_definition`
|
||||
2. Creates all stages with their configuration
|
||||
3. Preserves `surface_pkg_id` references (the referenced package must
|
||||
be installed separately)
|
||||
4. Sets the workflow as inactive (admin activates manually)
|
||||
|
||||
## Stage Modes
|
||||
|
||||
Each stage has a `stage_mode` that determines the built-in surface:
|
||||
|
||||
| Mode | Surface | Description |
|
||||
|------|---------|-------------|
|
||||
| `chat_only` | Chat | AI conversation with the visitor |
|
||||
| `form_only` | Form | Structured data collection, no AI |
|
||||
| `form_chat` | Form + Chat | Form fields alongside AI chat |
|
||||
| `review` | Review | Human reviewer sees collected data, approve/reject |
|
||||
|
||||
## Custom Stage Surfaces (surface_pkg_id)
|
||||
|
||||
Any stage can override its built-in surface with a custom package
|
||||
surface by setting `surface_pkg_id` to an installed package ID.
|
||||
|
||||
### How It Works
|
||||
|
||||
1. Admin sets `surface_pkg_id` on a stage (via dropdown in stage editor
|
||||
or API)
|
||||
2. When a visitor reaches that stage, the workflow template checks for
|
||||
`surface_pkg_id`
|
||||
3. If set, it dynamically loads `/surfaces/{pkg_id}/js/main.js`
|
||||
4. The package JS registers a surface via
|
||||
`sw.workflow.registerSurface(name, factory)`
|
||||
5. The surface is mounted into the stage container with context
|
||||
|
||||
### Writing a Custom Surface
|
||||
|
||||
Create a package with `js/main.js`:
|
||||
|
||||
```js
|
||||
// js/main.js — Custom review dashboard surface
|
||||
(function() {
|
||||
sw.workflow.registerSurface('review-dashboard', function(container, ctx) {
|
||||
// ctx contains:
|
||||
// channelId — workflow instance channel
|
||||
// sessionId — visitor session ID
|
||||
// basePath — API base path
|
||||
// stageMode — the stage_mode value
|
||||
// formTemplate — form schema (if applicable)
|
||||
// totalStages — number of stages in workflow
|
||||
// currentStage — current stage index (0-based)
|
||||
|
||||
container.innerHTML = '<div class="review-panel">Loading...</div>';
|
||||
|
||||
// Fetch workflow data
|
||||
sw.workflow.getContext(ctx.channelId).then(function(status) {
|
||||
container.innerHTML = renderReview(status);
|
||||
});
|
||||
|
||||
return {
|
||||
mount: function() { /* called when surface attaches */ },
|
||||
unmount: function() { /* called when surface detaches */ }
|
||||
};
|
||||
});
|
||||
})();
|
||||
```
|
||||
|
||||
Package manifest:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "review-dashboard",
|
||||
"title": "Review Dashboard",
|
||||
"type": "surface",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
### Setting surface_pkg_id
|
||||
|
||||
**Via admin UI:**
|
||||
|
||||
Admin → Workflows → select workflow → click Edit on a stage →
|
||||
Custom Surface Package dropdown → select a package.
|
||||
|
||||
The dropdown shows all installed packages of type `surface`, `workflow`,
|
||||
or `full`. Stages with a custom surface show a `pkg: <id>` badge in the
|
||||
stage list.
|
||||
|
||||
**Via API:**
|
||||
|
||||
```bash
|
||||
# Set custom surface
|
||||
curl -X PUT /api/v1/workflows/:wf_id/stages/:stage_id \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Review",
|
||||
"ordinal": 2,
|
||||
"stage_mode": "review",
|
||||
"history_mode": "summary",
|
||||
"surface_pkg_id": "review-dashboard"
|
||||
}'
|
||||
|
||||
# Clear (revert to built-in)
|
||||
curl -X PUT /api/v1/workflows/:wf_id/stages/:stage_id \
|
||||
-d '{ "name": "Review", "ordinal": 2, "stage_mode": "review",
|
||||
"history_mode": "summary", "surface_pkg_id": null }'
|
||||
```
|
||||
|
||||
Note: stage update is **PUT** (full replace), not PATCH. All stage
|
||||
fields must be included in the request body.
|
||||
|
||||
### Fallback Behavior
|
||||
|
||||
- `surface_pkg_id = null` → built-in surface based on `stage_mode`
|
||||
- `surface_pkg_id` references a missing/disabled package → error logged,
|
||||
falls back to built-in surface
|
||||
- Package deleted → `ON DELETE SET NULL` clears the reference
|
||||
automatically (PostgreSQL FK constraint)
|
||||
|
||||
## Starlark Workflow Module
|
||||
|
||||
Extensions with the `workflow.access` permission can interact with
|
||||
workflows from Starlark code.
|
||||
|
||||
### workflow.get_definition(workflow_id)
|
||||
|
||||
Returns the workflow definition including all stages.
|
||||
|
||||
```python
|
||||
def on_tool_call(tool_name, params):
|
||||
wf = workflow.get_definition(params["workflow_id"])
|
||||
# wf: { id, name, slug, stages: [{ name, stage_mode, surface_pkg_id, ... }] }
|
||||
return {"stages": len(wf["stages"])}
|
||||
```
|
||||
|
||||
### workflow.get_stage_data(channel_id)
|
||||
|
||||
Returns the current stage data for a workflow instance.
|
||||
|
||||
```python
|
||||
data = workflow.get_stage_data(channel_id)
|
||||
# data: { current_stage, stage_data, ... }
|
||||
```
|
||||
|
||||
### workflow.advance(channel_id)
|
||||
|
||||
Programmatically advance to the next stage.
|
||||
|
||||
```python
|
||||
workflow.advance(channel_id)
|
||||
```
|
||||
|
||||
### workflow.reject(channel_id, reason)
|
||||
|
||||
Reject the current stage and go back.
|
||||
|
||||
```python
|
||||
workflow.reject(channel_id, "Missing required information")
|
||||
```
|
||||
|
||||
## Form Template Schema
|
||||
|
||||
Stages with `form_only` or `form_chat` mode use `form_template` to
|
||||
define structured data collection.
|
||||
|
||||
### Field Types
|
||||
|
||||
| Type | Renders As | Validation |
|
||||
|------|-----------|------------|
|
||||
| `text` | Text input | `min_length`, `max_length`, `pattern` |
|
||||
| `email` | Email input | Format validation |
|
||||
| `number` | Number input | `min`, `max` |
|
||||
| `date` | Date picker | `min_date`, `max_date` |
|
||||
| `select` | Dropdown | `options` array required |
|
||||
| `textarea` | Multi-line text | `min_length`, `max_length` |
|
||||
| `checkbox` | Checkbox | — |
|
||||
| `file` | File upload | — |
|
||||
|
||||
### Field Schema
|
||||
|
||||
```json
|
||||
{
|
||||
"fields": [
|
||||
{
|
||||
"key": "full_name",
|
||||
"type": "text",
|
||||
"label": "Full Name",
|
||||
"required": true,
|
||||
"placeholder": "Enter your name",
|
||||
"min_length": 2,
|
||||
"max_length": 100
|
||||
},
|
||||
{
|
||||
"key": "department",
|
||||
"type": "select",
|
||||
"label": "Department",
|
||||
"required": true,
|
||||
"options": ["Engineering", "Sales", "Marketing", "HR"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Validation Hooks
|
||||
|
||||
Packages with `forms.validate` permission can provide custom validation:
|
||||
|
||||
```python
|
||||
# script.star
|
||||
def validate(fields, stage_data):
|
||||
errors = []
|
||||
if fields.get("email", "").endswith("@competitor.com"):
|
||||
errors.append({"field": "email", "message": "Invalid email domain"})
|
||||
return errors # empty list = valid
|
||||
|
||||
def on_submit(fields, stage_data):
|
||||
# Called after validation passes
|
||||
# Can transform or enrich data before it's saved
|
||||
return fields
|
||||
```
|
||||
|
||||
## Database Schema
|
||||
|
||||
### workflow_stages table
|
||||
|
||||
| Column | PG Type | SQLite Type | Description |
|
||||
|--------|---------|-------------|-------------|
|
||||
| `id` | `UUID` | `TEXT` | Stage ID |
|
||||
| `workflow_id` | `UUID` | `TEXT` | Parent workflow FK |
|
||||
| `ordinal` | `INTEGER` | `INTEGER` | Stage order (0-based) |
|
||||
| `name` | `TEXT` | `TEXT` | Display name |
|
||||
| `persona_id` | `UUID` | `TEXT` | AI persona FK (nullable) |
|
||||
| `assignment_team_id` | `UUID` | `TEXT` | Team assignment FK (nullable) |
|
||||
| `form_template` | `JSONB` | `TEXT` | Form field schema |
|
||||
| `stage_mode` | `TEXT` | `TEXT` | `chat_only\|form_only\|form_chat\|review` |
|
||||
| `history_mode` | `TEXT` | `TEXT` | `full\|summary\|fresh` |
|
||||
| `auto_transition` | `BOOLEAN` | `INTEGER` | Auto-advance on completion |
|
||||
| `transition_rules` | `JSONB` | `TEXT` | Conditional transition config |
|
||||
| `surface_pkg_id` | `TEXT` | `TEXT` | Custom surface package FK (nullable) |
|
||||
| `created_at` | `TIMESTAMPTZ` | `TEXT` | Creation timestamp |
|
||||
|
||||
The `surface_pkg_id` column has a foreign key to `packages(id)` with
|
||||
`ON DELETE SET NULL` in PostgreSQL. SQLite enforces FK constraints via
|
||||
`PRAGMA foreign_keys = ON`.
|
||||
Reference in New Issue
Block a user