From e708a76701eddee87d1a5438657c050e0411320f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Sat, 30 May 2026 22:53:57 +0100 Subject: [PATCH] docs: document portable workflow import/export YAML format (#1176) --- .../backend/config/workflows/architecture.yml | 5 + apps/backend/config/workflows/feature-dev.yml | 5 + .../config/workflows/improve-kandev.yml | 5 + apps/backend/config/workflows/kanban.yml | 5 + .../config/workflows/office-default.yml | 5 + .../config/workflows/plan-and-build.yml | 5 + apps/backend/config/workflows/pr-review.yml | 5 + apps/backend/config/workflows/routine.yml | 5 + docs/workflow-import-export.md | 398 ++++++++++++++++++ docs/workflow-tips.md | 4 + 10 files changed, 442 insertions(+) create mode 100644 docs/workflow-import-export.md diff --git a/apps/backend/config/workflows/architecture.yml b/apps/backend/config/workflows/architecture.yml index 2dd5794c9e..e629d3f32c 100644 --- a/apps/backend/config/workflows/architecture.yml +++ b/apps/backend/config/workflows/architecture.yml @@ -1,3 +1,8 @@ +# Internal workflow TEMPLATE definition — NOT the portable import/export format. +# Steps here reference each other by string `step_id`; the importable YAML uses a +# `version`/`type`/`workflows` envelope and `step_position`. Don't paste this file +# (or its `step_id` transitions) into the workflow import endpoint. +# Format reference: docs/workflow-import-export.md id: architecture name: Architecture description: >- diff --git a/apps/backend/config/workflows/feature-dev.yml b/apps/backend/config/workflows/feature-dev.yml index 4a7449f814..9d55e14a92 100644 --- a/apps/backend/config/workflows/feature-dev.yml +++ b/apps/backend/config/workflows/feature-dev.yml @@ -1,3 +1,8 @@ +# Internal workflow TEMPLATE definition — NOT the portable import/export format. +# Steps here reference each other by string `step_id`; the importable YAML uses a +# `version`/`type`/`workflows` envelope and `step_position`. Don't paste this file +# (or its `step_id` transitions) into the workflow import endpoint. +# Format reference: docs/workflow-import-export.md id: feature-dev name: Feature Dev description: >- diff --git a/apps/backend/config/workflows/improve-kandev.yml b/apps/backend/config/workflows/improve-kandev.yml index 16d0410a59..fb278e037d 100644 --- a/apps/backend/config/workflows/improve-kandev.yml +++ b/apps/backend/config/workflows/improve-kandev.yml @@ -1,3 +1,8 @@ +# Internal workflow TEMPLATE definition — NOT the portable import/export format. +# Steps here reference each other by string `step_id`; the importable YAML uses a +# `version`/`type`/`workflows` envelope and `step_position`. Don't paste this file +# (or its `step_id` transitions) into the workflow import endpoint. +# Format reference: docs/workflow-import-export.md id: improve-kandev name: Improve Kandev description: >- diff --git a/apps/backend/config/workflows/kanban.yml b/apps/backend/config/workflows/kanban.yml index 65c3b57ec4..f3eea43c83 100644 --- a/apps/backend/config/workflows/kanban.yml +++ b/apps/backend/config/workflows/kanban.yml @@ -1,3 +1,8 @@ +# Internal workflow TEMPLATE definition — NOT the portable import/export format. +# Steps here reference each other by string `step_id`; the importable YAML uses a +# `version`/`type`/`workflows` envelope and `step_position`. Don't paste this file +# (or its `step_id` transitions) into the workflow import endpoint. +# Format reference: docs/workflow-import-export.md id: simple name: Kanban description: >- diff --git a/apps/backend/config/workflows/office-default.yml b/apps/backend/config/workflows/office-default.yml index 11641b778e..5fa54a7a0c 100644 --- a/apps/backend/config/workflows/office-default.yml +++ b/apps/backend/config/workflows/office-default.yml @@ -1,3 +1,8 @@ +# Internal workflow TEMPLATE definition — NOT the portable import/export format. +# Steps here reference each other by string `step_id`; the importable YAML uses a +# `version`/`type`/`workflows` envelope and `step_position`. Don't paste this file +# (or its `step_id` transitions) into the workflow import endpoint. +# Format reference: docs/workflow-import-export.md id: office-default name: Office Default description: >- diff --git a/apps/backend/config/workflows/plan-and-build.yml b/apps/backend/config/workflows/plan-and-build.yml index 6862737c4c..02489fd1a1 100644 --- a/apps/backend/config/workflows/plan-and-build.yml +++ b/apps/backend/config/workflows/plan-and-build.yml @@ -1,3 +1,8 @@ +# Internal workflow TEMPLATE definition — NOT the portable import/export format. +# Steps here reference each other by string `step_id`; the importable YAML uses a +# `version`/`type`/`workflows` envelope and `step_position`. Don't paste this file +# (or its `step_id` transitions) into the workflow import endpoint. +# Format reference: docs/workflow-import-export.md id: standard name: Plan & Build description: >- diff --git a/apps/backend/config/workflows/pr-review.yml b/apps/backend/config/workflows/pr-review.yml index bb3f51c63f..ecc44a7309 100644 --- a/apps/backend/config/workflows/pr-review.yml +++ b/apps/backend/config/workflows/pr-review.yml @@ -1,3 +1,8 @@ +# Internal workflow TEMPLATE definition — NOT the portable import/export format. +# Steps here reference each other by string `step_id`; the importable YAML uses a +# `version`/`type`/`workflows` envelope and `step_position`. Don't paste this file +# (or its `step_id` transitions) into the workflow import endpoint. +# Format reference: docs/workflow-import-export.md id: pr-review name: PR Review description: >- diff --git a/apps/backend/config/workflows/routine.yml b/apps/backend/config/workflows/routine.yml index a1cc1fe282..8dd9e9e2c6 100644 --- a/apps/backend/config/workflows/routine.yml +++ b/apps/backend/config/workflows/routine.yml @@ -1,3 +1,8 @@ +# Internal workflow TEMPLATE definition — NOT the portable import/export format. +# Steps here reference each other by string `step_id`; the importable YAML uses a +# `version`/`type`/`workflows` envelope and `step_position`. Don't paste this file +# (or its `step_id` transitions) into the workflow import endpoint. +# Format reference: docs/workflow-import-export.md id: routine name: Routine description: System workflow for routine-fired tasks. Single auto-completing step. diff --git a/docs/workflow-import-export.md b/docs/workflow-import-export.md new file mode 100644 index 0000000000..1d7a45cff9 --- /dev/null +++ b/docs/workflow-import-export.md @@ -0,0 +1,398 @@ +# Workflow Import / Export — Portable YAML Format + +Kandev workflows can be exported to a portable YAML file and imported into +another workspace (or another Kandev install). This page is the reference for +that file format: the envelope, every field, the trigger config shapes, and the +matching rules applied on import. + +Everything below is derived from the source of truth: + +- `apps/backend/internal/workflow/models/export.go` — the portable structs (`WorkflowExport`, `WorkflowPortable`, `StepPortable`, `AgentProfilePortable`) and `Validate()`. +- `apps/backend/internal/workflow/models/models.go` — the `StepEvents` triggers and their action types. +- `apps/backend/internal/workflow/service/service.go` — `ImportWorkflows` / `importSingleWorkflow` (matching + position→ID mapping). + +> **Looking for the built-in workflows instead?** See [Workflows](workflow-tips.md) +> for the default templates and when to use each. + +--- + +## How import/export is exposed + +The format is YAML over three HTTP endpoints (all under `/api/v1`): + +| Method | Path | Purpose | Response / Body | +|--------|------|---------|-----------------| +| `GET` | `/workflows/:id/export` | Export a single workflow | `application/x-yaml` | +| `GET` | `/workspaces/:id/workflows/export` | Export **all** workflows in a workspace | `application/x-yaml` | +| `POST` | `/workspaces/:id/workflows/import` | Import workflows into a workspace | YAML request body, **max 1 MB** | + +Export marshals the structs to YAML; import unmarshals the request body, runs +`Validate()`, then creates each workflow. The same struct shapes also marshal to +JSON (every field carries both `yaml` and `json` tags), but the endpoints speak +YAML. + +--- + +## Envelope + +The top-level document is a `WorkflowExport`: + +```yaml +version: 1 +type: kandev_workflow +workflows: + - # WorkflowPortable … + - # WorkflowPortable … +``` + +| Field | Type | Required | Notes | +|-------|------|:--------:|-------| +| `version` | int | yes | Must be exactly `1` (`ExportVersion`). Any other value is rejected. | +| `type` | string | yes | Must be exactly `kandev_workflow` (`ExportType`). | +| `workflows` | list | yes | Must contain at least one workflow; an empty list is rejected. | + +`Validate()` rejects the document with a descriptive error if `version`, +`type`, or the workflow list fails these checks. + +--- + +## `WorkflowPortable` + +Each entry under `workflows:`: + +```yaml +- name: My Workflow + description: Optional human description. + agent_profile: # optional, workflow-level default agent + agent_name: Claude Code + model: claude-opus-4-7 + mode: default + steps: + - # StepPortable … +``` + +| Field | Type | Required | Notes | +|-------|------|:--------:|-------| +| `name` | string | yes | Workflow name. Required by `Validate()`. Used for **dedup on import** (see [Import rules](#import-matching-rules)). | +| `description` | string | no | Omitted from export when empty. | +| `agent_profile` | object | no | Workflow-level default agent profile. See [Agent profiles](#agent-profiles). Omitted when the workflow has no profile. | +| `steps` | list | — | The workflow's steps. See [`StepPortable`](#stepportable). | + +Instance-specific fields (IDs, timestamps, workspace association) are **not** +part of the portable format — they are regenerated on import. + +--- + +## `StepPortable` + +Each entry under `steps:`: + +```yaml +- name: In Progress + position: 1 + color: bg-blue-500 + prompt: | + Optional per-step prompt sent to the agent. + is_start_step: true + show_in_command_panel: true + allow_manual_move: true + auto_archive_after_hours: 24 + agent_profile: # optional, step-level agent override + agent_name: Claude Code + model: claude-opus-4-7 + mode: default + events: + # triggers — see "Triggers" below +``` + +| Field | Type | Required | Default in export | Notes | +|-------|------|:--------:|-------------------|-------| +| `name` | string | yes | — | Required by `Validate()`. | +| `position` | int | yes | always emitted | **0-based** index. **Must be unique** within the workflow — duplicates are rejected. Also the anchor for `move_to_step` references (see below). | +| `color` | string | — | always emitted | Tailwind background class, e.g. `bg-blue-500`, `bg-green-500`, `bg-neutral-400`. | +| `prompt` | string | no | omitted when empty | Per-step prompt template. Supports placeholders such as `{{task_prompt}}`. | +| `events` | object | — | always emitted | Triggers and their actions. See [Triggers](#triggers). | +| `is_start_step` | bool | — | always emitted | Marks the step new tasks start in. | +| `show_in_command_panel` | bool | — | always emitted | Whether the step appears in the command panel. | +| `allow_manual_move` | bool | — | always emitted | Whether users can drag the task into this step manually. | +| `auto_archive_after_hours` | int | no | omitted when `0` | Auto-archive a task this many hours after it lands in the step. `0` / omitted = never. | +| `agent_profile` | object | no | omitted when none | Step-level agent profile, overriding the workflow default. See [Agent profiles](#agent-profiles). | + +> **Note:** `position`, `color`, `events`, and the three booleans carry no +> `omitempty`, so they always appear in exported files (even when `false` or +> empty). The `prompt`, `auto_archive_after_hours`, and `agent_profile` fields +> are omitted when unset. + +> **Not in the portable format:** office/Phase-2 step metadata — `stage_type`, +> step participants (reviewers/approvers), and recorded decisions — is **not** +> exported or imported. Only the fields listed above round-trip. + +--- + +## Agent profiles + +`agent_profile` appears at both the workflow level and the step level +(`AgentProfilePortable`): + +```yaml +agent_profile: + agent_name: Claude Code # required + model: claude-opus-4-7 # optional, omitted when empty + mode: default # optional, omitted when empty +``` + +| Field | Type | Notes | +|-------|------|-------| +| `agent_name` | string | The agent's **display name** (`AgentDisplayName`), not its internal ID. | +| `model` | string | Model identifier. Omitted when empty. | +| `mode` | string | Agent mode. Omitted when empty. | + +Profiles are stored by **value** (name/model/mode) rather than by ID precisely +so they can be re-matched in a different workspace. See the matching rule below. + +--- + +## Triggers + +`events` holds the step's triggers. Each trigger is a list of actions; an +action is `{ type, config }` where `config` is an optional map. + +There are two families of triggers. + +### Kanban-era triggers (round-trip today) + +These four triggers use typed action slices and are fully supported by +import/export: + +| Trigger | Allowed action `type`s | `config` | +|---------|------------------------|----------| +| `on_enter` | `enable_plan_mode`, `auto_start_agent`, `reset_agent_context`, `clear_decisions`, `queue_run`, `queue_run_for_each_participant` | first four take no config; `queue_run` / `queue_run_for_each_participant` use the same config keys as the office triggers (see [Office triggers](#office--phase-2-triggers-intended-format--see-caveat)) | +| `on_turn_start` | `move_to_next`, `move_to_previous`, `move_to_step` | `move_to_step` needs `step_position` | +| `on_turn_complete` | `move_to_next`, `move_to_previous`, `move_to_step`, `disable_plan_mode` | `move_to_step` needs `step_position` | +| `on_exit` | `disable_plan_mode` | — | + +Example: + +```yaml +events: + on_enter: + - type: auto_start_agent + on_turn_start: + - type: move_to_next + on_turn_complete: + - type: move_to_step + config: + step_position: 2 +``` + +#### `move_to_step` uses `step_position`, not `step_id` + +This is the one transformation the portable format performs. Internally a step +transition references a target step by its database `step_id`. Because IDs are +not portable, export rewrites every `move_to_step` action's +`config.step_id` → `config.step_position`, and import rewrites it back to a +freshly generated `step_id`. + +So in a portable file you **always** write: + +```yaml +- type: move_to_step + config: + step_position: 2 # the target step's `position`, NOT a step id +``` + +`Validate()` enforces that every `move_to_step` `step_position` matches an +existing step's `position` in the same workflow; a dangling reference is +rejected. Any additional keys in the action's `config` are preserved verbatim +through the conversion. + +> **Built-in template YAMLs differ.** The embedded templates under +> `apps/backend/config/workflows/*.yml` use string `step_id`s (e.g. +> `step_id: review`) because they are *template definitions*, a different schema +> from this portable export format. Do not copy their `step_id:` form into a +> portable import file — use `step_position:`. + +### Office / Phase-2 triggers (intended format — see caveat) + +The seven event-driven "office" triggers use the generic action shape +(`GenericAction`): + +| Trigger | Fires when | +|---------|-----------| +| `on_comment` | A comment is added to the task. | +| `on_blocker_resolved` | A blocker on the task is resolved. | +| `on_children_completed` | All child tasks complete. | +| `on_approval_resolved` | An approval request is decided. | +| `on_heartbeat` | A periodic heartbeat tick. | +| `on_budget_alert` | A budget threshold is crossed. | +| `on_agent_error` | The agent errors out. | + +Each holds a list of generic actions whose `type` is one of `queue_run`, +`clear_decisions`, or `queue_run_for_each_participant`, with a free-form +`config` map interpreted by the engine. Common keys: `target` (e.g. `primary`, +`workspace.ceo_agent`), `task_id` (e.g. `this`), `reason`, and `role`. + +Intended shape: + +```yaml +events: + on_comment: + - type: queue_run + config: + target: primary + task_id: this + reason: task_comment + on_children_completed: + - type: queue_run + config: + target: primary + task_id: this + reason: children_completed +``` + +> [!WARNING] +> **These seven triggers do not round-trip today** (tracked by +> [#1109](https://github.com/kdlbs/kandev/issues/1109)). The portable +> conversion (`remapStepEvents` in `export.go`) only copies `on_enter`, +> `on_turn_start`, `on_turn_complete`, and `on_exit`. As a result the office +> triggers are **dropped on export** (they never appear in an exported file) and +> **dropped on import** (if you hand-author them, they are parsed but discarded +> before the step is persisted). The format above documents the *intended* +> shape; until #1109 lands, office-style workflows will not survive a +> round-trip. Coordinate with that fix before relying on it. + +--- + +## Import matching rules + +`ImportWorkflows` → `importSingleWorkflow` applies these rules: + +1. **Validation first.** The whole document is run through `Validate()` + (envelope + per-workflow name/position/`move_to_step` checks) before + anything is created. A failure aborts the entire import. + +2. **Workflow dedup by name.** Existing workflows in the target workspace are + listed; any imported workflow whose `name` already exists is **skipped** + (reported under `skipped`). The rest are **created** (reported under + `created`). The result is `{ created: [...], skipped: [...] }`. + +3. **Fresh step IDs + position→ID mapping.** Every step gets a newly generated + UUID. Import builds a `position → new step ID` map, then rewrites each + `move_to_step` action's `step_position` back into the new `step_id`. This is + why step positions must be unique and why `move_to_step` references positions. + +4. **Agent profile matched by name/model/mode.** For each `agent_profile` + (workflow-level and step-level), import searches the target workspace's + agents and profiles for one whose **`agent_name` (display name), `model`, and + `mode` all match exactly**. On a match, that profile's ID is assigned. On **no + match**, the field is left empty — the workflow/step is created **without** an + agent profile (silently; no error). Match the names/models/modes in the + target workspace if you need the profile wired up. + +--- + +## Complete worked example + +A self-contained, valid import file with two workflows: a four-step kanban loop +(using `move_to_step` with `step_position`) and a two-step planning flow with a +per-step agent profile and prompt. + +```yaml +version: 1 +type: kandev_workflow +workflows: + - name: Simple Kanban + description: Assign → run → review loop. + steps: + - name: Backlog + position: 0 + color: bg-neutral-400 + is_start_step: false + show_in_command_panel: false + allow_manual_move: true + events: + on_turn_start: + - type: move_to_next + + - name: In Progress + position: 1 + color: bg-blue-500 + is_start_step: true + show_in_command_panel: true + allow_manual_move: true + events: + on_enter: + - type: auto_start_agent + on_turn_complete: + - type: move_to_step + config: + step_position: 2 + + - name: Review + position: 2 + color: bg-yellow-500 + is_start_step: false + show_in_command_panel: true + allow_manual_move: true + events: + on_turn_start: + - type: move_to_previous + + - name: Done + position: 3 + color: bg-green-500 + is_start_step: false + show_in_command_panel: false + allow_manual_move: true + auto_archive_after_hours: 168 + events: + on_turn_start: + - type: move_to_step + config: + step_position: 1 + + - name: Plan & Build + description: Plan first, then implement. + agent_profile: + agent_name: Claude Code + model: claude-opus-4-7 + mode: default + steps: + - name: Plan + position: 0 + color: bg-purple-500 + is_start_step: true + show_in_command_panel: true + allow_manual_move: true + prompt: | + Analyze the task and produce an implementation plan. Do not write code. + Save the plan with create_task_plan_kandev, then stop for review. + agent_profile: + agent_name: Claude Code + model: claude-opus-4-7 + mode: plan + events: + on_enter: + - type: enable_plan_mode + - type: auto_start_agent + on_exit: + - type: disable_plan_mode + + - name: Implementation + position: 1 + color: bg-blue-500 + is_start_step: false + show_in_command_panel: true + allow_manual_move: true + prompt: | + Retrieve the plan with get_task_plan_kandev, then implement it. + events: + on_enter: + - type: auto_start_agent +``` + +Importing this into a fresh workspace creates both workflows +(`created: [Simple Kanban, Plan & Build]`). Re-importing the same file leaves +them untouched (`skipped: [Simple Kanban, Plan & Build]`). The `Claude Code` +agent profiles wire up only if a profile with that exact display name, model, +and mode exists in the target workspace. diff --git a/docs/workflow-tips.md b/docs/workflow-tips.md index 4b0b30a1fa..0c0117222d 100644 --- a/docs/workflow-tips.md +++ b/docs/workflow-tips.md @@ -4,6 +4,10 @@ Kandev ships with five workflow templates. Each defines a sequence of steps with You can use these as-is or **create custom workflows** tailored to you or your team. +> **Sharing workflows?** Workflows can be exported to and imported from a +> portable YAML file. See [Workflow Import / Export](workflow-import-export.md) +> for the full format reference and a worked example. + ## Default Workflows ### Kanban