message the API returns.
Set these before you run the commands on this page:
message is the short reason. detail sometimes carries more context.
Quick checks
Run these first. They answer most “why did this happen” questions.Check the workflow version and its validation report
is_active and validation_report. A version with validation_report.ok: false cannot be activated. Its errors list each problem as "path: message".Check which version the ticket is on
workflow_version_id with the version you edited. Tickets stay on the version that was active when they were created.Read the transition history
from_label, to_label, actor_kind (user or system), actor_id, reason, and occurred_at. A due-date breach appears as actor_kind: "system" with reason: "due policy breach".Symptom index
Workflow validation and activation
Creating or patching a workflow always stores a new version, even when the spec is invalid. The problems are recorded invalidation_report. Activation is where an invalid spec is refused. Run validate first to see every error without storing anything:
/validate returns 200 whether or not the spec is valid. Read data.valid and data.errors:
Graph errors: G1, G2, G4
Graph errors: G1, G2, G4
Permit errors
Permit errors
Guard CEL errors
Guard CEL errors
foo == 1. A guard must return bool. You get cel: guard expression must evaluate to bool, got … otherwise.Guards can read ticket, actor, event, now, queue, project, and http. Inside ticket, use id, metadata, assigned_to, assigned_group_id, tagged_user_ids, and tags.Action errors (on_enter / on_exit)
Action errors (on_enter / on_exit)
Validation rule and due policy errors
Validation rule and due policy errors
Trigger intake errors (T1–T12)
Trigger intake errors (T1–T12)
T11: group_by and dedupe_key are both set; group_by takes precedence at runtime is a warning, not an error. The dedupe_key is ignored.A field I set seems to be ignored
A field I set seems to be ignored
permits instead of permit disappears without an error. Re-read the stored spec from GET …/ticket-workflows/{workflow_id} and compare it with what you sent.Validate returns 400 "workflow is required" or "invalid workflow"
Validate returns 400 "workflow is required" or "invalid workflow"
- “workflow is required”: The body has no
workflowkey. This also applies to YAML bodies. SendContent-Type: application/yamland put the spec under a top-levelworkflow:key. - “invalid workflow”: The spec could not be parsed, or
nameorinitialis missing (detailsaysworkflow name and initial are required).
PATCH returns 400 "workflow yaml name must match existing workflow"
PATCH returns 400 "workflow yaml name must match existing workflow"
name in the body must match it. To start a separate workflow, POST a new one with a different name.POST /ticket-workflows with a name that already exists in the Project does not fail. It adds a new version to that workflow.Template instantiate returns 409
Template instantiate returns 409
Deleting workflows
DELETE …/ticket-workflows/{workflow_id} deletes one version, not the whole definition.
Ticket create rejected
404 "active workflow not found"
404 "active workflow not found"
- You passed a workflow version ID instead of the definition ID.
workflow_def_idcomes from theworkflow_def_idfield of a workflow item, not itsid. - The definition has no active version. Activate one with
POST …/ticket-workflows/{workflow_id}/activate.
400 "on_create requires tag kind …"
400 "on_create requires tag kind …"
X in validation.on_create.require_tag_kinds. Add it in initial_tags:writes_to.kind: tag can also supply the tag.400 "Invalid tag value or reference" and other tag errors
400 "Invalid tag value or reference" and other tag errors
400 "on_create requires form payload" or form errors
400 "on_create requires form payload" or form errors
validation.on_create.require_form. Send form_payload.require_form without version always uses the latest form version. Patching a form can therefore change what new tickets must send. See Forms.400 "invalid due_at"
400 "invalid due_at"
due_at must be RFC3339, for example 2026-10-12T17:00:00Z. Date-only values such as 2026-10-12 are rejected.400 "exactly one of assigned_user_id or assigned_group_id is required"
400 "exactly one of assigned_user_id or assigned_group_id is required"
403 on create, patch, or delete
403 on create, patch, or delete
Transition rejected
to_label is a label key such as in_progress, not a state_label_id.
400 "no such transition"
400 "no such transition"
to_label. This includes a misspelled or unknown to_label.- Get the ticket and note
workflow_version_id. - Read that version’s
spec.transitions. Do not read the newest version. - Pick a
tothat has a matchingfromfor the current label.
403 "transition permit denied"
403 "transition permit denied"
permit entries. Check:400 "guard failed" or "guard expression could not be evaluated"
400 "guard failed" or "guard expression could not be evaluated"
- “guard failed”: A guard returned
false. The transition is refused and nothing changes. - “guard expression could not be evaluated”: The guard errored at runtime, for example by reading a tag or metadata key that is not there, or by taking longer than 50 ms. Use
has()before reading optional values:
400 "missing required tag kind … before transition"
400 "missing required tag kind … before transition"
validation.before_transition rule for this from → to lists X in require_tag_kinds. Add the tag with POST …/tickets/{ticket_id}/tags, then retry.400 "transition A -> B requires form payload"
400 "transition A -> B requires form payload"
before_transition rule for this edge has require_form. Send form_payload in the transition body:409 "cannot transition to terminal: N open subticket(s) remain"
409 "cannot transition to terminal: N open subticket(s) remain"
subticket tags pointing at child tickets that are still open or in_progress. Close or cancel the children first.subticket tag on the parent. Sub-tickets created in the Console are linked through metadata.parent_ticket_id on the child, which this check does not read. A parent whose Console sub-tickets are still open can therefore still close. Add subticket tags to the parent if you need the server to enforce it.409 "ticket state changed concurrently" or "in progress"
409 "ticket state changed concurrently" or "in progress"
- “ticket state changed concurrently”: Another transition moved the ticket while yours was running. Get the ticket again and decide from its new state.
- “in progress”: You retried with the same
Idempotency-Keywhile the first request was still running. Wait, then retry with the same key to receive the original result.
504 Gateway Timeout
504 Gateway Timeout
Idempotency-Key header on creates and transitions so a retry does not apply the change twice.Workflow edits not applied to existing tickets
Symptom: You patched a workflow and activated the new version, but existing tickets still follow the old transitions, guards, actions, or due policy. Cause: Each ticket is pinned to theworkflow_version_id that was active when it was created. Transitions, permits, guards, forms, actions, and due policy all come from that version. Activating a new version only affects tickets created afterward.
Fix:
- Expect old tickets to finish on the old version. Their transitions are listed in that version’s
spec. - Patching does not activate. Call
POST …/ticket-workflows/{workflow_id}/activatewith the new version’sid. - State label IDs are per version. Filter lists with
labels(keys) rather thanstate_label_idwhen tickets span versions. See Listing and filtering.
Transition actions
on_exit webhook sees the ticket already in the new state
on_exit webhook sees the ticket already in the new state
on_exit ticket.http call fires, the ticket is already in the target state. Within one transition, on_exit actions are picked up before on_enter actions.Do not design an on_exit hook that needs to observe the old state. Pass what you need in the request instead, or read it from the transition history.The webhook or device call never happened
The webhook or device call never happened
ticket.http action counts as failed when:- the URL is refused (private or link-local address, unresolvable host)
- the response status is outside
200–299 - a response binding’s
frompath is missing from the response body
ticket.rpc action counts as failed when a CEL expression in device_id or fields errors, or the call cannot be queued. It succeeds when the call is queued, not when the device runs it. Check the device side with Queued invoke.Action outcomes are not exposed on the ticket API. To see what happened:- check your endpoint’s access logs, or the device’s RPC audit
- check
metadataon the ticket for values that a response binding should have written - make the endpoint idempotent, since an action can be retried after a slow partial success
Actions use the wrong URL after I edited the workflow
Actions use the wrong URL after I edited the workflow
Trigger intake
Trigger intake failures do not return an error to anyone. The event is skipped and no ticket appears. Work through these causes in order.No tickets are created
No tickets are created
Duplicate tickets from the same condition
Duplicate tickets from the same condition
Alarm did not open a work order
Alarm did not open a work order
trigger_intake. Check that:- the alarm definition has
auto_create_woenabled - the alarm instance is attached to an asset
wo_workflow_def_idpoints at a workflow with an active version. If you leave it empty, the Project needs an active workflow withkind: work_order- the cooldown has passed.
wo_cooldowndefaults to24h, and the default dedup key is per asset
Due date and breach
The due date passed and the ticket did not move
The due date passed and the ticket did not move
- The ticket’s workflow version has a
due_policywithon_breach.to. - The ticket has a top-level
due_at, set on create or withPATCH …/tickets/{ticket_id}. - A transition exists from the ticket’s current label to
on_breach.to, withsystemin itspermit. - That edge has no
before_transitionrule withrequire_form, because a timer cannot fill a form. - The edge’s guards pass. If the target is terminal, the ticket has no open
subticketchildren. - The ticket is not already in a terminal state.
No "Ticket Due Soon" or "Ticket Due" notifications
No "Ticket Due Soon" or "Ticket Due" notifications
- Without a
due_policyin the ticket’s workflow version, no due-date timers are registered, even whendue_atis set. - Ticket Due Soon needs
due_policy.lead_time. It is skipped whendue_atminuslead_timeis already in the past. - Moving a ticket to a terminal state cancels its timers.
"Ticket Due" fires right after the ticket is created
"Ticket Due" fires right after the ticket is created
due_at in the past, or very close to now, fires the breach almost immediately. This is expected.Notifications
Start with the delivery log for the subscription. Every person a role selects is either sent a message or listed with a reason.$API/projects/$PROJECT_ID/notification-subscriptions and need manage integrations on the Project. Organization subscriptions need manage integrations on the Organization. See Assignment and notifications.
Rate limits
429 “rate limit exceeded” means you sent too many requests per second to one route. The limit is shared by every caller in your Organization (keyed on ORG-ID).
Listing and filtering
metadata from a single ticket:
Next steps
- Workflows: labels, transitions, permits, guards, and due policy
- Forms: schemas, bindings, and versions
- Automation: actions, trigger intake, and alarm work orders
- Assignment and notifications
- Console guide
- API reference