Skip to main content
A workflow defines the states a ticket can be in and the transitions between them. You write it as a spec (JSON or YAML), check it with the validate endpoint, save it as a version, and activate that version so new tickets use it. This page is the reference for the spec. For forms, see Forms. For trigger_intake, due_policy and transition actions in depth, see Automation. For every route, see API reference.

Spec structure

A spec missing name or initial cannot be parsed at all — the validate endpoint returns 400, and create or patch fails. Every other problem shows up as a validation error (see Graph validation). Run the spec through the validate endpoint before you save it.

Labels

Each label is a state a ticket can be in. Declare labels in labels.extra: The label’s class becomes the ticket’s system_class when the ticket enters that label. Use system_class to filter and report across workflows that use different label keys.
Set terminal: true on every end-state label, even when its class is already terminal_success, terminal_failed or cancelled. The class alone sets closed_at, but only terminal: true publishes the Ticket Closed event (and its notifications) and stops the ticket’s due-date timers.

Imported labels

labels.import lists keys from the Project’s shared state-label catalog. Each imported key must already exist in that catalog, or creating the workflow fails. Imported labels take their class and color from the catalog and are never terminal, so they do not count toward the terminal-state rules below. There is no API to manage the catalog today — declare your labels in labels.extra.

Initial state

initial names the label a new ticket starts in. It must be a declared label key. If the workflow has trigger_intake entries, the initial label must be in labels.extra with class: open.

Transitions

Each entry in transitions is one allowed edge: Each from → to pair can appear only once. To move a ticket, call POST …/tickets/{ticket_id}/transition with {"to_label": "<key>"}. A target with no edge from the ticket’s current label returns 400 no such transition.

Permits

A transition succeeds when any entry in permit matches the caller. If no entry matches, the transition returns 403 transition permit denied.
Validation only checks that each permit is a reserved value or has the shape <entity>.<permission>. A misspelled entity or permission passes validation and then never matches. Test each edge with a real user before you rely on it.
An empty permit list fails validation with at least one permit clause is required. Use [system] for edges that only automation should take.

What happens on a transition

The platform checks a transition in this order and stops at the first failure. Everything runs in one transaction, so a failure leaves the ticket unchanged. Because the form is applied in step 3, guards and tag checks see the metadata and tags the form just wrote.

Guards with CEL

Guards are CEL expressions that must evaluate to bool. Validation compiles every guard and rejects non-boolean expressions. At transition time each guard has a 50 ms evaluation budget. event, queue and http are declared but empty when a guard runs. CEL string extensions are available (lowerAscii(), split(), replace() and so on). Reading a key that does not exist is an evaluation error, which returns 400 guard expression could not be evaluated. Guard optional keys with has(). A guard that returns false returns 400 guard failed. Examples:

Validation rules

validation adds requirements on top of permits and guards.

On create

require_form and optional_form cannot both be set.

Before a transition

Each before_transition entry applies to one declared from → to edge: Omit version to use the latest version of a catalog form. Inline forms in forms.extra take precedence over catalog forms of the same name. See Forms.

Transition actions

on_exit and on_enter list actions to run when a transition happens. Two kinds are supported: Actions are queued in the same transaction as the state change and run asynchronously after it commits — the transition response does not wait for them, and an action failure does not undo the transition. For each transition, on_exit actions run before on_enter actions. ticket.http URLs must use http or https and must resolve to a public address when you validate the workflow. Private and link-local addresses are rejected. See Automation for retries, bindings and RPC details.

Work-order workflows

Set kind: work_order to mark a workflow as a CMMS work-order workflow. CMMS features look for workflows of this kind — for example, an alarm that creates work orders without a named workflow uses the Project’s active work_order workflow. name is only a label. A workflow with no kind whose name is work_order is also treated as a work-order workflow. The cmms.work_order workflow template installs a ready-made work-order workflow you can edit.

Sub-tickets and terminal states

A parent ticket links a child with a tag of kind subticket whose value is the child ticket’s UUID. While any linked child has system_class open or in_progress, the parent cannot move to a terminal label (terminal: true, or class terminal_success, terminal_failed or cancelled). The transition returns:
The Console links sub-tickets by setting metadata.parent_ticket_id on the child. This check does not read that field. Only subticket tags on the parent block terminal transitions.

Graph validation

Validation runs these checks on every spec. Errors block activation; warnings do not. A label counts as terminal when it has terminal: true or class terminal_success, terminal_failed or cancelled.

Validate a spec

POST /projects/{project_id}/ticket-workflows/validate checks a spec without saving it. Send JSON with the spec under workflow, or a YAML document with a top-level workflow: key and Content-Type: application/yaml.
The response always returns 200 when the spec parses. Check valid:
A body that cannot be parsed, or a spec without name or initial, returns 400.

Full example

A maintenance-request workflow: tickets need a tagged device, someone must be assigned before work starts, resolving requires a resolution form, and resolving posts a message to a webhook. Both files below are complete request bodies for validate, create and patch.
Replace the webhook URL with your own. To resolve a ticket on this workflow, send the form answers with the transition:

Versioning and activation

A workflow definition (workflow_def_id) holds a list of immutable versions. Routes that take {workflow_id} expect a version id.
1

Create a version

POST /projects/{project_id}/ticket-workflows with the spec. The response data.item includes id (version id), workflow_def_id, version, is_active, spec and validation_report. A new version is never active.
2

Check the validation report

The version is saved even when it has errors. validation_report.ok is false and validation_report.errors lists them as "<path>: <message>" strings.
3

Activate the version

POST /projects/{project_id}/ticket-workflows/{workflow_id}/activate. This deactivates any other version of the same workflow. A version with validation errors returns 400 workflow validation failed.
4

Create tickets

Pass workflow_def_id to POST …/tickets. New tickets always use the definition’s active version.
Existing tickets stay on the version they were created with. Their transitions, permits, guards and actions keep following that version after you activate a newer one.

State labels endpoint

GET /projects/{project_id}/ticket-workflows/{workflow_id}/state-labels lists the labels of one workflow version, with the ids tickets reference in state_label_id.
An unknown version id returns 404 workflow not found.