Skip to main content
Each entry below follows the same shape: the symptom you see, the cause, and the fix. Error text in quotes is the exact message the API returns. Set these before you run the commands on this page:
Errors use this envelope. message is the short reason. detail sometimes carries more context.

Quick checks

Run these first. They answer most “why did this happen” questions.
1

Check the workflow version and its validation report

Look at is_active and validation_report. A version with validation_report.ok: false cannot be activated. Its errors list each problem as "path: message".
2

Check which version the ticket is on

Compare workflow_version_id with the version you edited. Tickets stay on the version that was active when they were created.
3

Read the transition history

Each row shows 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 in validation_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:
Validation only checks the permit’s shape. A well-formed permit that names an entity or permission that does not exist passes validation but never matches anyone at runtime. See Workflows for the permit reference.
Guards are compiled at validation time. An undeclared variable fails, for example 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.
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.
Unknown fields in a workflow spec are dropped silently, so a typo such as 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.
  • “workflow is required”: The body has no workflow key. This also applies to YAML bodies. Send Content-Type: application/yaml and put the spec under a top-level workflow: key.
  • “invalid workflow”: The spec could not be parsed, or name or initial is missing (detail says workflow name and initial are required).
A patch adds a version to an existing workflow, so the 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.
“A workflow or form with this template’s name already exists in the project”. You already have a workflow or form with the template’s name, and it was not created from this template. Rename or remove the existing one, then retry. Instantiating needs the manage assets permission.

Deleting workflows

DELETE …/ticket-workflows/{workflow_id} deletes one version, not the whole definition.

Ticket create rejected

  • You passed a workflow version ID instead of the definition ID. workflow_def_id comes from the workflow_def_id field of a workflow item, not its id.
  • The definition has no active version. Activate one with POST …/ticket-workflows/{workflow_id}/activate.
The workflow lists X in validation.on_create.require_tag_kinds. Add it in initial_tags:
A form binding with writes_to.kind: tag can also supply the tag.
Each initial tag is checked against its kind. Typical messages:
The workflow has 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.
due_at must be RFC3339, for example 2026-10-12T17:00:00Z. Date-only values such as 2026-10-12 are rejected.
You set both. A ticket has at most one assignee. Send one of them, or neither.Related messages: “user is not a project member” (the user must belong to the Project) and “user group not found in this organization”.

Transition rejected

to_label is a label key such as in_progress, not a state_label_id.
The ticket’s workflow version has no edge from the ticket’s current label to to_label. This includes a misspelled or unknown to_label.
  1. Get the ticket and note workflow_version_id.
  2. Read that version’s spec.transitions. Do not read the newest version.
  3. Pick a to that has a matching from for the current label.
The caller matches none of the edge’s permit entries. Check:Add a permit to the edge in a new workflow version, or assign or tag the right person on the ticket.
  • “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:
A 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.
A before_transition rule for this edge has require_form. Send form_payload in the transition body:
Form validation errors are the same as on create.
The parent has subticket tags pointing at child tickets that are still open or in_progress. Close or cancel the children first.
The server only counts children linked by a 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.
  • “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-Key while the first request was still running. Wait, then retry with the same key to receive the original result.
The platform did not answer in time. The change may still have been applied. Get the ticket or its transition history before you retry. Send an 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 the workflow_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}/activate with the new version’s id.
  • State label IDs are per version. Filter lists with labels (keys) rather than state_label_id when tickets span versions. See Listing and filtering.

Transition actions

Actions run after the transition is committed, not during it. By the time an 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.
Actions start within a few seconds of the transition. A failed action is retried up to 3 attempts, 15 seconds apart. After the last attempt the action is dropped from the queue. The transition itself is not rolled back.A 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 from path is missing from the response body
A 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 metadata on 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 come from the ticket’s pinned workflow version. Tickets on the old version keep calling the old URL. See Workflow edits not applied.

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.
Alarm-to-work-order creation is set on the alarm definition, not in trigger_intake. Check that:
  • the alarm definition has auto_create_wo enabled
  • the alarm instance is attached to an asset
  • wo_workflow_def_id points at a workflow with an active version. If you leave it empty, the Project needs an active workflow with kind: work_order
  • the cooldown has passed. wo_cooldown defaults to 24h, and the default dedup key is per asset
If no workflow resolves, the alarm still fires and no ticket is created. See Alarms.

Due date and breach

A breach moves a ticket only when all of these hold:
  1. The ticket’s workflow version has a due_policy with on_breach.to.
  2. The ticket has a top-level due_at, set on create or with PATCH …/tickets/{ticket_id}.
  3. A transition exists from the ticket’s current label to on_breach.to, with system in its permit.
  4. That edge has no before_transition rule with require_form, because a timer cannot fill a form.
  5. The edge’s guards pass. If the target is terminal, the ticket has no open subticket children.
  6. The ticket is not already in a terminal state.
When the move is skipped, the Ticket Due notification still fires.
The Console’s Due date field stores metadata.due_at. Only the top-level due_at field drives due-date timers, warnings, and breaches. Set due_at through the API when you rely on due_policy.
  • Without a due_policy in the ticket’s workflow version, no due-date timers are registered, even when due_at is set.
  • Ticket Due Soon needs due_policy.lead_time. It is skipped when due_at minus lead_time is already in the past.
  • Moving a ticket to a terminal state cancels its timers.
A 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.
Project subscriptions live under $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). Back off and retry. For bulk work, pace creates to stay under 12 per second.

Listing and filtering

Decode metadata from a single ticket:

Next steps