Skip to main content

Machine definitions

A machine definition is a plain JSON document that describes a finite-state machine: where it starts, what states it can be in, and how it moves between them. You publish a definition once, then create as many instances of it as you need — each instance walks the same states independently.

Overview

At its simplest, a machine is an initial state plus a map of states. An optional context object holds the running data bag that travels with every instance. Each state declares the events it responds to and which state each event transitions to.

Definitions are published with the SDK (client.machines.publish(...)) or the HTTP API (POST /api/machines). Every publish creates a new immutable version; existing instances keep running on the version they were created with.

The definition object

The top-level shape has three fields:

FieldTypeRequiredDescription
initialstringYesName of the state every new instance starts in. Must be a key in states.
contextobjectNoDefault data bag merged into each instance at creation time.
states{ [name]: StateDefinition }YesMap of state name to its definition. See below.
{
  "initial": "draft",
  "context": { "approvals": 0 },
  "states": {
    "draft": { "on": { "SUBMIT": "review" } },
    "review": { "on": { "APPROVE": "published", "REJECT": "draft" } },
    "published": { "type": "final" }
  }
}

States

Each entry in states is a state definition with these fields, all optional:

FieldTypeDescription
type"atomic" | "final"Defaults to atomic. A final state moves the instance status to done and accepts no further events.
on{ [EVENT]: Transition }Map of event type to the transition it triggers.
entryActionDefinition[]Actions to run when the state is entered. Executed live as part of the ordered list from.exit ++ transition.actions ++ to.entry.
exitActionDefinition[]Actions to run when the state is left. Executed live (see entry).
after{ delay, target, ... }Optional timeout transition. Entering the state arms a Durable Object alarm that fires the synthetic orchestate.after event (generation-gated so a stale alarm is a no-op).
compensationActionDefinition[]Saga undo actions for this state. Pushed onto a per-instance stack on entry; unwound LIFO when a transition with compensate: true fires.

Note — entry and exit actions use the same shared executor as transition actions (assign, log, http, emit). Side effects are best-effort and never fail the committed transition. See the Actions section below.

Transitions

A transition tells the engine which state to move to when an event arrives. It can be written two ways.

Shorthand string form

When you only need to name the target state, set the event directly to the target name as a string:

{
  "states": {
    "draft": {
      "on": { "SUBMIT": "review" }
    }
  }
}

Object form

The object form lets you attach a guard and per-transition actions alongside the target:

{
  "states": {
    "review": {
      "on": {
        "APPROVE": {
          "target": "published",
          "guard": "hasQuorum",
          "actions": [
            { "type": "assign", "params": { "approvedAt": "now" } }
          ]
        }
      }
    }
  }
}
FieldTypeDescription
targetstringRequired. Name of the destination state.
guardstringOptional guard expression, evaluated before the transition fires. Supports context.<path> / event.payload.<path> refs, comparisons (== != < <= > >=) and exists()/!exists().
actionsActionDefinition[]Optional actions to run while taking the transition.

Note — Guard evaluation is live. A guard that evaluates false rejects the send with 422 and code: "GUARD_REJECTED". New publishes validate guard syntax (unparseable guards are a 400); guards on machines published before validation existed still pass with a runtime warning for backwards compatibility.

Actions

An action is a small instruction attached to a transition, a state's entry/exit, or a state's compensation list. The shape is:

{
  "type": "assign" | "log" | "emit" | "http" | "agent_step",
  "params": { /* type-specific options */ }
}
TypeStatusWhat it does
assignLiveMerges params into the instance context (pure engine fold).
logLiveRecords a structured log line (level, message, fields).
emitLiveDelivers a signed webhook (X-Orchestate-Signature, HMAC-SHA256) to every enabled endpoint configured for your organization, carrying the transition and the action params. Best-effort, off the response path.
httpLiveOutbound HTTPS call (SSRF-screened: https-only, no private/loopback). Optional authEnvVar injects a dedicated worker secret as Authorization: Bearer. Best-effort, 5s timeout.
agent_stepRecordedRecords a bounded governance intent for Symphony-style runs. By default the intent is logged only — no worker execution is claimed unless a dispatcher is injected.

Note — assign is folded into context by the pure FSM engine.log, http, and emit run through the shared action executor on both the REST and console event paths. Side effects never fail or block the committed transition.

Context

Context is the running data bag carried by every instance. It starts from the optional top-level context object (and anything you pass to client.instances.create({ machineSlug, context })), and is updated as the instance runs by assign actions, which shallow-merge their params into it.

{
  "states": {
    "review": {
      "on": {
        "APPROVE": {
          "target": "published",
          "actions": [
            { "type": "assign", "params": { "status": "live", "approvals": 1 } }
          ]
        }
      }
    }
  }
}

You can read an instance’s current state and context from a transition result, or watch them change in real time via client.instances.subscribe(...) from an authenticated browser session.

Worked example: document approval

A complete definition for a three-stage document-approval workflow. It starts in draft, moves through review, and ends in the final published state. The APPROVE transition uses an assign action to stamp the context.

{
  "initial": "draft",
  "context": {
    "title": "",
    "approvals": 0
  },
  "states": {
    "draft": {
      "on": {
        "SUBMIT": "review"
      }
    },
    "review": {
      "on": {
        "APPROVE": {
          "target": "published",
          "actions": [
            { "type": "assign", "params": { "approvals": 1, "status": "live" } }
          ]
        },
        "REJECT": "draft"
      }
    },
    "published": {
      "type": "final"
    }
  }
}

Publish it with the SDK, where slug identifies the machine for future instance creation:

import { OrchestateClient } from '@orchestate/sdk';

const client = new OrchestateClient({ apiKey: process.env.ORCHESTATE_API_KEY! });

await client.machines.publish({
  slug: 'doc-approval',
  name: 'Document approval',
  definition: {
    initial: 'draft',
    context: { title: '', approvals: 0 },
    states: {
      draft: { on: { SUBMIT: 'review' } },
      review: {
        on: {
          APPROVE: {
            target: 'published',
            actions: [{ type: 'assign', params: { approvals: 1, status: 'live' } }],
          },
          REJECT: 'draft',
        },
      },
      published: { type: 'final' },
    },
  },
});

Validation rules

When you publish, Orchestate validates the request and rejects invalid definitions:

  • slug must match ^[a-z0-9-]+$ (lowercase letters, digits, and hyphens) and be 1–80 characters long.
  • initial and states are required. Make sure initial names a state defined in states — an instance cannot start otherwise.
  • An invalid body returns 400 with a JSON error of the form { "error": "<message>" }.
  • Publishing is rate-limited to 100 requests/min/org; exceeding it returns 429.

See also

  • SDK reference — publish definitions and drive instances in TypeScript.
  • HTTP API — the underlying REST endpoints and error codes.