Assigning a workflow to an (organisation, product)¶
Every (organisation, product) combination on the platform must
have an active workflow before cases can be opened against it.
Without one, clinical-api refuses case creation with
422 missing_workflow_definition (see PR #154). This runbook walks
through how to provision, edit, and reassign workflows from the admin
UI.
Mental model¶
- A
WorkflowDefinitionbelongs to exactly one(orgId, productId)slot and carries aversionnumber that's unique within that slot (@@unique([orgId, productId, version])). - Workflows move through statuses:
draft→active→archived. Onlyactiverows satisfy the case-creation gate. - Reassignment (changing the
(org, product)) is allowed on draft rows only. The orchestrator renumbersversionto the next free value under the new slot so the unique constraint doesn't bite the operator. - A product-wide default workflow has
orgId IS NULLfor a specific product. The runtime resolver falls back to the default when no org-specific active workflow exists for the (org, product) pair the caller is hitting. Only cross-tenant admins can author or promote a draft to default (isDefault: trueon create/update); a 409 fires if another active default already exists for the same product.
Where to find the status¶
Open Organisations → click an org → Products tab. The new Workflow column shows one of three things for each assigned product:
| Cell shows | Meaning | Action |
|---|---|---|
<workflow name> + green active badge |
The slot has a published workflow; case creation will succeed. | Click the name to open the workflow detail page. |
<workflow name> + yellow draft badge |
A draft exists but hasn't been published; cases for this slot will be refused. | Click in to publish (or delete + start over). |
| Create workflow link | No workflow row yet; cases for this slot will be refused. | Click to deep-link into the new-workflow form with this (org, product) pre-selected. |
<workflow name> + blue default badge |
A product-wide default workflow (orgId IS NULL) applies — the org inherits it. |
No action needed; case creation will succeed via the resolver fallback. |
Provisioning a new workflow¶
- From Org → Products, click Create workflow in the Workflow column. The new-workflow page opens with the Organisation + Product dropdowns already filled in.
- Set Name, Default mode (
client_drivenfor operator- authored step graphs,activefor server-driven), and the Workflow timeout (seconds). Click Create draft. - The editor opens. Build the step graph using the palette on the left.
- Save draft as you go. Validation runs after each save and surfaces any errors that would block publish.
- Publish when ready. Status transitions to
active; the workflow now satisfies the case-creation gate.
The first publish unblocks case creation for the slot. Subsequent versions follow the same flow (clone or "new version" on the detail page → edit → publish), with the platform auto-archiving the previous active row.
Authoring a product-wide default¶
On the new-workflow form, tick Default for all organisations.
The Organisation dropdown is replaced with a hint — the workflow
will apply to every org that has the chosen product, until that org
authors its own override. Only cross-tenant admins see / can use the
checkbox; tenant clients sending is_default: true are refused with 403. Promoting an existing draft to default is also possible from
the workflow-edit Assignment card; the orchestrator refuses with 409
if another active default already exists for the product (archive
the incumbent first).
Editing an existing draft¶
On the workflow-edit page (/workflows/<id>/edit):
- The header carries the workflow name, status badge, version, and Save/Publish buttons (unchanged).
- The Assignment card sits below the header. It's editable
while the workflow is
draftand read-only afterwards.
The Assignment card lets you reassign a draft workflow to a different
(org, product) without re-creating it. Common reasons:
- Operator typo on creation. Picked the wrong org/product on the new-workflow form; move it before publish.
- Lift a working draft to a sibling org/product. When a draft
authored against one slot becomes the template you want to use
elsewhere — you can also use the
cloneaction on the detail page if you want to keep a copy under the original slot.
How the version renumber works¶
If you change the (org, product), the orchestrator queries the
target slot for its current highest version and stamps the
moved workflow with version = max + 1. The unique constraint
(orgId, productId, version) therefore never trips when
reassigning. Same-slot saves (same org, same product) keep the
existing version unchanged.
Published / archived workflows can't be reassigned¶
The Assignment card switches to read-only when status is active
or archived. Re-binding a live workflow would change which slot
its instances belong to and break the audit chain. If you need to
move a published workflow:
- Archive it (so case creation falls back to the
missing_workflow_definitionerror or to a different active version). - Clone it to the target slot via the detail page's clone action.
- Publish the clone.
Failure modes you'll hit¶
| Symptom | What it means | Fix |
|---|---|---|
POST /v1/clinical-api/cases → 422 missing_workflow_definition |
No active workflow for this (org, product). |
Create + publish one. |
PATCH /workflow-definitions/:id → 409 Only draft definitions can be edited |
Trying to reassign a published / archived workflow. | Archive + clone instead. |
PATCH … → 403 Tenant clients can only assign workflows to their own org |
A tenant-scoped client (oauth2 client_credentials) tried to move a workflow to a different org via the API. | Use a cross-tenant admin to perform the move. |
Two drafts in the same slot, both with version: 1 |
(shouldn't be possible — unique constraint enforces it) | If you see this, file a bug. |
Backend / API notes¶
The reassignment surface lives on the existing PATCH endpoint — no new routes:
PATCH /v1/orchestrator/workflow-definitions/{id}
Content-Type: application/json
Authorization: Bearer <admin-token>
{
"orgId": "<new-org-id>",
"productId": "<new-product-id>"
}
Other PATCH fields (name, description, definition,
defaultMode, workflowTimeoutSeconds) continue to work
alongside reassignment.
clinical-api enforces the gate via the orchestrator's internal endpoint:
GET /workflow-definitions/active?org_id=<id>&product_id=<id>
Authorization: Bearer <SERVICE_AUTH_TOKEN>
Returns { hasActive: boolean, definitionId?: string }. Used by
CasesService.create() before persisting a case. The check walks
the resolver fallback (org-specific → product-wide default) so a
product-wide default satisfies the gate for every org with that
product. See docs/audiences/tech/services/orchestrator.md for the
full contract, plus the GET /workflow-definitions/resolve route
external consumers use to fetch the active workflow without knowing
its id.