ErstellenAutomatisierung
Diese Seite ist noch nicht übersetzt; angezeigt wird die englische Fassung.

Workflows

Model a record's lifecycle as a state machine — the states it can be in, the moves allowed between them, and nothing a flow can do better.

A workflow models a record's lifecycle as a finite state machine: the states the record can be in and the moves allowed between them. You declare it as a state_machine validation rule on the object, and every write to the state field is checked against it. Use one when the core requirement is "this object can only move through these states."

There is no standalone Salesforce-style Workflow Rule authoring type. The old "workflow" concept splits cleanly in two:

  • A state_machine validation rule for strict lifecycle transitions — this page.
  • Flows for event-triggered or scheduled automation, including approval pauses.

Workflows vs flows

Workflow (state machine)Flow
ModelsState — where a record is in its lifecycleSteps — what happens when something occurs
Answers"Is this transition allowed right now?""What do we do about it?"
ShapeA state field and a table of allowed movesNodes and edges: triggers, actions, branches
Side effectsNone — it only constrainsAll of them — email, updates, HTTP, waits

They compose: the state machine constrains the transitions; flows perform the side effects around them when you need notifications, record updates, or external calls.

Define a state machine

A support case that must move new → assigned → resolved, with an escalation path. The state machine is one of the object's validations:

import { P } from '@objectstack/spec';
import { ObjectSchema, Field } from '@objectstack/spec/data';

export const SupportCase = ObjectSchema.create({
  name: 'support_case',
  label: 'Support Case',
  fields: {
    status: Field.select({
      label: 'Status',
      required: true,
      options: [
        { label: 'New', value: 'new', default: true },
        { label: 'Assigned', value: 'assigned' },
        { label: 'Escalated', value: 'escalated' },
        { label: 'Resolved', value: 'resolved' },
      ],
    }),
    resolution: Field.textarea({
      label: 'Resolution',
      requiredWhen: P`record.status == 'resolved'`,  // no resolved case without one
    }),
  },
  validations: [
    {
      type: 'state_machine',
      name: 'case_lifecycle',
      field: 'status',
      message: 'This status change is not allowed.',
      initialStates: ['new'],
      transitions: {
        new: ['assigned'],
        assigned: ['resolved', 'escalated'],
        escalated: ['resolved'],
        resolved: [],
      },
    },
  ],
});

Read it as a contract: a case is created as new, and a new case can only be assigned. An assigned case can be resolved or escalated, and an escalated one can be resolved. resolved lists nothing, so nothing moves it again. Because resolution is required when the status is resolved, the write that resolves a case without one is refused.

Anatomy

KeyWhat it declares
type: 'state_machine'This validation rule is a state machine
nameThe rule's identifier (snake_case)
fieldThe state field the rule governs
transitionsMap of each state → the states it may move to; checked when the record is updated
initialStatesThe states a record may be created in; checked when it is inserted
messageThe error a refused write returns

The rule also takes the keys every validation rule shares: severity, events, active and priority.

Final states

A state whose list is empty is final: every move out of it is refused. A state with no key at all is not final. The check has nothing to reason about there and lets the write through, so declare [] when you mean "final".

Conditions on a transition

A state_machine rule has no per-transition guard. To make a move conditional, put the condition where the platform enforces it:

  • A value a state needs — requiredWhen on that field, as resolution does above. It refuses the write that enters the state without the value, and leaves rows that predate the rule alone.
  • Anything else — a sibling script or conditional validation rule (a CEL predicate) in the same validations array.

Writes, not events

The machine is checked on writes to the state field. An update that changes status to a value not listed under its current value is refused, and so is an insert outside initialStates. There are no named events to declare: the table of legal moves is the whole machine.

Tip: Keep the machine minimal — states and the moves between them. The moment you want "and then send an email", you've left workflow territory: put the side effect in a flow that reacts to the transition.

Side effects belong in flows

State machines describe valid transitions — nothing else. When a transition should do something (notify sales, stamp a closed date, call an external system), pair the machine with a flow triggered by the record change:

export const dealClosedWon = defineFlow({
  name: 'deal_closed_won',
  label: 'Deal closed won',
  type: 'record_change',
  status: 'active',
  nodes: [
    {
      id: 'start',
      type: 'start',
      label: 'Stage moved to closed won',
      config: {
        triggerType: 'record-after-update',
        objectName: 'opportunity',
        condition: "record.stage == 'closed_won' && previous.stage != 'closed_won'",
      },
    },
    {
      id: 'set_closed_date',
      type: 'update_record',
      label: 'Set Closed Date',
      config: {
        objectName: 'opportunity',
        filter: { id: '{record.id}' },
        fields: { closed_date: '{TODAY()}' },
      },
    },
    {
      id: 'notify_sales',
      type: 'notify',
      label: 'Notify Sales',
      config: { recipients: '{record.owner}', title: 'Deal won: {record.name}' },
    },
    { id: 'end', type: 'end', label: 'End' },
  ],
  edges: [
    { id: 'e1', source: 'start', target: 'set_closed_date' },
    { id: 'e2', source: 'set_closed_date', target: 'notify_sales' },
    { id: 'e3', source: 'notify_sales', target: 'end' },
  ],
});

The machine guarantees the deal reached closed_won legally; the flow handles what happens next. Conditions like the one above are CEL expressions — see CEL.

Migrating from workflow rules

If you're coming from a platform with Workflow Rules, here's the mapping:

Old conceptCurrent equivalent
Workflow RuleFlow
Time trigger ("N days before/after a date")Time-relative trigger — config.timeRelative on a flow's start node (16.0, see Flows)
Time trigger (fixed clock)Scheduled flow
Field update actionupdate_record node
Email alertnotify node
HTTP callhttp node
Approval ProcessFlow with one or more approval nodes

The test: if the old rule was "when X happens, do Y", model it as a small flow. If it was "N days before/after a date field, do Y", declare a time-relative trigger — never a date-equality condition on a record-change flow, which only evaluates when the record happens to change. If it was "this record must move through controlled states", declare a state_machine validation rule and use flows for the side effects.

Where to go next

PageWhy
FlowsSide effects around transitions — triggers, steps, error handling
ApprovalsHuman sign-off as a pause inside a flow
Validation rulesThe rule family a state machine belongs to
CEL expressionsThe language behind requiredWhen, validation rules and flow conditions
Data modelingThe objects whose lifecycles you're constraining
Automation overviewChooser: flow vs workflow vs approval