REST API Workflow - edmozley/freeitsm GitHub Wiki

โš™๏ธ REST API: Workflow

The complete usage guide for the Workflow module of the REST API โ€” automation rules, test-firing, the engine's run history, and the machine-readable trigger/action catalogues. Mirrors the interactive documentation at System โ†’ API โ†’ Documentation (with its live "Try it" tester).

[!NOTE] New to the API? The basics table on REST API: Tickets covers base URL, authentication, the response envelope, error codes, rate limits and PATCH semantics โ€” identical across all modules.

What's distinctive about Workflow:

๐Ÿ“š The engine's catalogues are the contract trigger_event, condition operators and action types are validated at write time against the engine's own catalogues โ€” nothing unexecutable can be stored. Unknown operators are a 422 (the UI stores them and the condition then silently fails at run time).
๐Ÿ”ฅ Test-fire over the API POST /workflows/{id}/fire is the editor's "Test fire" button: synthetic payload in, real actions out, the full per-step log back โ€” and production run stats (run_count, last_run) stay untouched.
๐Ÿงพ Run history that survives deletion Executions are the engine's audit trail. Deleting a workflow keeps its runs โ€” they're detached (workflow.id becomes null) but stay attributable via a name snapshot taken at run time. ?orphaned=true finds them.
๐Ÿž A debugging surface the UI only hints at A single execution read returns the complete step log: every condition with the actual value it saw and whether it passed, every action with its result or error. The editor's sidebar shows only the last 20 runs' summaries.
โš ๏ธ Powerful grants Workflow actions run with engine privileges โ€” they create tickets, send email from the ticket's mailbox, assign analysts. Treat workflows.create / workflows.update / workflows.fire as admin-level permissions and scope them to trusted keys.

๐Ÿš€ Quick start โ€” build a rule, test it, watch it run

B="https://your-server/api/v1"; K="Authorization: Bearer fitsm_โ€ฆ"

# What can I trigger on, and what can I do? (no guessing โ€” ask the engine)
curl -s "$B/workflow-triggers" -H "$K"
curl -s "$B/workflow-actions"  -H "$K"

# Create a rule: note every urgent ticket
WF=$(curl -s -X POST "$B/workflows" -H "$K" -H "Content-Type: application/json" -d '{
  "name": "Flag urgent tickets",
  "trigger_event": "ticket.created",
  "conditions": [ { "field": "ticket.subject", "op": "contains", "value": "urgent" } ],
  "actions": [ { "type": "add_ticket_note",
                 "args": { "ticket_id": "{{ticket.id}}", "note": "Auto-flagged as urgent." } } ]
}' | jq '.data'); ID=$(echo "$WF" | jq -r '.id')

# Test-fire it with a synthetic payload โ€” real actions, stats untouched
curl -s -X POST "$B/workflows/$ID/fire" -H "$K" -H "Content-Type: application/json" \
  -d '{ "payload": { "ticket": { "id": 123, "subject": "urgent: server down" } } }'

# Watch it run for real
curl -s "$B/workflows/$ID/executions" -H "$K"

โš™๏ธ Workflows

๐ŸŸข GET /workflows ย  ๐Ÿ”‘ workflows.read

All rules, most recently updated first, with run stats and conditions_count / actions_count (the full rule bodies come on the single read). Filters: trigger_event, is_active=true|false, q (names + descriptions), company (a company id, or shared). Each workflow has a company - see Companies.

๐Ÿ”ต POST /workflows ย  ๐Ÿ”‘ workflows.create

name and trigger_event required. conditions is an array of {field, op, value} (AND semantics โ€” empty means always fire); actions is an ordered array of {type, args}. Zero actions is allowed (draft-friendly, matching the editor). Action args support {{dot.path}} template variables resolved from the event payload โ€” "ticket_id": "{{ticket.id}}" is the idiom that binds an action to the triggering ticket.

Everything is validated against the engine's catalogues: unknown triggers, operators and action types are a 422 pointing you at GET /workflow-triggers / GET /workflow-actions.

๐ŸŸข GET /workflows/{id} ย  ๐Ÿ”‘ workflows.read

The full rule โ€” decoded conditions and actions, creator, run stats:

{ "data": { "id": 7, "name": "Flag urgent tickets", "trigger_event": "ticket.created",
    "is_active": true, "created_by": { "id": 1, "name": "Administrator" },
    "conditions": [ { "field": "ticket.subject", "op": "contains", "value": "urgent" } ],
    "actions": [ { "type": "add_ticket_note", "args": { "ticket_id": "{{ticket.id}}", "note": "โ€ฆ" } } ],
    "last_run": { "at": "2026-07-04T08:42:44Z", "status": "success" }, "run_count": 12 } }

๐ŸŸ  PATCH /workflows/{id} ย  ๐Ÿ”‘ workflows.update

Partial update โ€” sent fields are validated like create. {"is_active": false} pauses a rule without losing it; the engine only picks up active workflows on dispatch.

๐Ÿ”ด DELETE /workflows/{id} ย  ๐Ÿ”‘ workflows.delete

Hard delete. Execution history survives โ€” runs are detached (workflow.id โ†’ null) but keep the workflow's name via the snapshot, so the audit trail stays readable forever.

๐Ÿ”ต POST /workflows/{id}/fire ย  ๐Ÿ”‘ workflows.fire

Runs the workflow immediately with a synthetic payload โ€” the editor's Test fire. The payload is whatever your conditions and {{templates}} read ({ "payload": { "ticket": { "id": 123, "subject": "โ€ฆ" } } }). Actions really execute (a create_ticket action really creates a ticket), but run_count / last_run are untouched so test runs don't pollute production stats. Returns the execution result inline:

{ "data": { "execution_id": 17, "status": "success",
    "step_log": [
      { "kind": "condition", "field": "ticket.subject", "op": "contains",
        "value": "urgent", "actual": "urgent: server down", "passed": true },
      { "kind": "action", "type": "add_ticket_note", "status": "success",
        "result": { "ticket_id": 123, "note_length": 24 } } ] } }

A run whose conditions don't match returns "status": "skipped" with the failing condition's actual value in the log โ€” the fastest way to debug a rule that "never fires".


๐Ÿงพ Executions

Read-only โ€” only the engine writes these. Statuses: running, success, failed, skipped (conditions didn't match), aborted (loop protection stepped in).

๐ŸŸข GET /workflows/{id}/executions ย  ๐Ÿ”‘ workflow_executions.read

One workflow's runs, newest first, paginated. Filters: status, trigger_event, started_since (ISO 8601).

๐ŸŸข GET /workflow-executions ย  ๐Ÿ”‘ workflow_executions.read

The install-wide run history โ€” including orphaned runs whose workflow has since been deleted (?orphaned=true), which the module's UI never surfaces. Same filters plus workflow_id.

๐ŸŸข GET /workflow-executions/{id} ย  ๐Ÿ”‘ workflow_executions.read

Full detail: the trigger_payload snapshot the engine saw, and the complete step_log โ€” every condition evaluated (with the actual value read from the payload) and every action's result or error message.


๐Ÿ“š Catalogues (reference data)

๐ŸŸข GET /workflow-triggers ย  ๐Ÿ”‘ reference.read

Every trigger event with its condition fields โ€” each field's dotted payload path, its type (lookup / numeric / text) and the operators valid for it. There are dozens - every module's events, the time-based ones and a created / updated / deleted trio per entity - so ask the endpoint rather than a list here. On an install with companies every trigger also offers company.id.

๐ŸŸข GET /workflow-actions ย  ๐Ÿ”‘ reference.read

Every action type with its full args spec โ€” labels, required flags, defaults, lookup sources and whether an arg supports {{template}} variables. Twenty-two today, among them assign_ticket_team, set_ticket_department and set_ticket_category (4.1.0) and set_ticket_type (4.8.0).

5.1.0. Each arg also carries options (a select's fixed choices, [{value, label}] โ€” send the value), new_default (what a newly added action starts with when it differs from default, e.g. add_ticket_note.write_to starts as note while a saved action with no write_to stays history-only) and show_when (the arg only matters when another arg has one of the listed values). The engine does not fill a missing arg in at run time โ€” send every arg you mean, defaults included.


๐Ÿข Companies

4.1.0. A workflow's company is null when it is shared - it runs for every company (and every workflow from before 4.1.0 is one) - or {id, name} when it runs for one company's events only. A run's company is the company its event belonged to (null = none known). On a single-company install company is always null and nothing below applies.

A key that reaches every company A key scoped to some companies
GET /workflows all shared + its companies' own
POST /workflows without company_id shared (as before) its default company
company_id: null (shared) on POST / PATCH yes 403
PATCH / DELETE / fire a shared workflow yes 403 - it acts on every company
another company's workflow by id - 404
runs all those whose event belonged to its companies; a run with no company is 404

A company's own workflow acts only on that company's records: an action naming another company's ticket fails, and the run's step_log says so. company_id on PATCH moves a workflow; the key needs the company it leaves and the one it goes to.

๐Ÿ“Œ Notes & gotchas

  • Actions run synchronously โ€” a fire request returns when the actions finish (a webhook only queues). Keep test payloads realistic but small.
  • Loop protection is request-scoped: a workflow can't re-enter itself, chains cap at depth 10, and 100 runs per request โ€” blocked runs are recorded as aborted executions, visible in the history.
  • The AI compose endpoints stay UI-only (Workflow โ†’ editor โ†’ AI compose) โ€” like every AI feature in the API so far.
  • The fire permission is separate from update precisely so you can hand a monitoring script the ability to trigger a runbook workflow without letting it rewrite the rule.

See also: REST API (how it all works) ยท Workflows (the module itself) ยท the other module guides linked from the sidebar.