Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Guides

Workflows

States, the events that move between them, who may raise each one, and what happens when a step waits for days. A workflow is source you read top to bottom, and a run that outlives a deploy.

01

The state is a column on your entity

A workflow does not carry a state of its own beside your data. It TRACKS a field you already declared — so "what stage is this order at" is one column, one index, and one thing to list on.

demo/wf-order-saga/model/order.osyverbatim — this file compiles
workflow OrderLifecycle {
1  Tracks    = Order.Status;
2  Autostart = true;
  Initial   = Placed;

3  event Pay();
  event Ship(string tracking);
  event Deliver();
4  [Authorize(u => RoleGrant.Any(g => g.Grantee == u && g.Level == AppRole.Manager))]
  event Cancel();

  on Cancel {
    if (this.Item.Status == OrderStatus.Shipped) {
      var ret = Workflow.Once("make-return", () => new ReturnShipment { Order = this.Item });
5      await Workflow.Run("ret", ret);          // hold until the return workflow refunds
      goto Returning;
    } else {
      goto Cancelled;
    }
  }

  state Placed {
    subscribe Pay();
    on Pay { goto Paid; }
  }

  state Paid {
6    subscribe Ship(string tracking) as Fulfil {
7      Candidates = u => RoleGrant.Any(g => g.Grantee == u && g.Level == AppRole.Fulfillment);
    }
8    on Fulfil(string tracking) {
      this.Item.Tracking = tracking;
      goto Shipped;
    }
  }

  state Shipped {
    subscribe Deliver();
    on Deliver { goto Delivered; }
  }

  terminal success Delivered { }
  terminal cancel  Cancelled { Message = "order cancelled"; }
9  terminal cancel  Returning { Message = "order cancelled after shipping — return + refund issued"; }
1

The workflow's state IS Order.Status. Not a parallel table joined to it. A screen that lists orders by stage is an ordinary query over an ordinary enum column.

2

A run begins when an Order row is created. Nothing starts it by hand, so there is no path where a row exists and its run does not.

3

The vocabulary of what can happen. An event may carry arguments — Ship(string tracking) — and those arguments are what the person doing the work is asked for.

4

Who may raise it, on the event itself. Not a check inside a handler that somebody can forget: an event a caller may not raise is an event they are never offered.

5

Awaiting a CHILD workflow parks this one. Durably: no process is held, and the order resumes when the return terminals — which may be minutes or a fortnight later. See the durability guide.

6

A slot — work offered to a set of people. This is the difference between a state machine and a workflow: the state says the order is paid, the slot says somebody has to ship it, and the slot is a row you can list, assign, chase and measure.

7

Who it is offered to, as a predicate over your own model. An inbox is then a query, not a feature.

8

The handler. goto moves the state — and moving it is the only thing that does, so the transition log is complete by construction.

9

Terminals are typed. success, cancel and error are three different endings, and a reporting screen that wants "how many failed" does not have to infer it from a status name.

02

Cancel from anywhere, and compensate

The on Cancel block sits outside every state, so it is reachable from all of them. What it does depends on where the order actually is — and cancelling something already shipped cannot just flip a flag, because the goods are gone.

on Cancel {
  if (this.Item.Status == OrderStatus.Shipped) {
    var ret = Workflow.Once("make-return", () => new ReturnShipment { Order = this.Item });
    await Workflow.Run("ret", ret);          // hold until the return workflow refunds
    goto Returning;
  } else {
    goto Cancelled;
  }
}

03

What happens when you change it

Workflow changes, and what they cost

Add a state
compiles
Additive. Existing runs are in the states they were in; new ones can reach the new one.
Remove a state that runs are parked in
refused
Refused at deploy without a migration. Stranding live work is the failure this exists to prevent, so it is caught when you deploy rather than when somebody opens the order.
Rename the tracked enum member
refused
Refused without a migration, and the migration is generated for you — the stored value is what every parked run resolves against.
Add an argument to an event
compiles
The people who satisfy that slot are asked for one more thing. A one-click move is no longer offered for it, and the UI knows that without being told.
Tighten Candidates
compiles
The inbox narrows. It is a predicate over your model, so it narrows the same way any other query would.

Where to go next

The durable engine

What `await` on a child actually costs, and the saga.

Agents

An agent's work runs inside one of these.

Workflow reference

36 pages, one construct each.