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

Reference / Workflow

Step labels (naming a child run so it survives a new version)

await Workflow.Run("label", entity) · await saga.Run("label", step) · await saga.Run("label", step, () => Undo(…))

Every child workflow you AWAIT carries a label — a literal string you choose, naming that step. Awaiting parks the run, sometimes for days, which is long enough for a new version of the app to be deployed underneath it; the label is what lets the new version recognise the step the run is sitting at and the work it has already finished. Two steps in one workflow may not share one. A fire-and-forget start never parks, so it needs none.

stable2 examples compiled by CIworkflowauthoringversioning

Summary#

await saga.Run("ship", shipment) and await Workflow.Run("fulfil", order) park the run: it stops, durably, until the child finishes. A three-step approval saga can sit parked for a week — and a week is long enough for the app to be deployed again.

When that happens, the new version has to answer one question about the parked run: where is it, and what has it already done? "Step 2 of 5" is only an answer if both versions agree on what "step 2" is. The label is that agreement, and it is why every step carries one.

Signature#

await Workflow.Run("fulfil", order);                        // label, then the entity
Workflow.Run(order);                                        // fire-and-forget — never parks, so no label
await saga.Run("reserve", hotel);                           // label, then the step
await saga.Run("reserve", hotel, () => Cancel(hotel));      // …and its compensation, as usual

The label is a literal string. It cannot be a variable or an expression: its whole job is to be the same string in a later version of the body, and a value computed while the run executes could differ between the two readings that have to match.

Description#

Why it is required rather than inferred#

The obvious convenience is to derive a label when you leave it out — from the child workflow's name, or from the step variable. Both are rejected, and the reason is worth stating plainly: a derived name is not one you chose. Rename the child workflow, or rename a local from hotel to outbound, and the derived label changes — silently, in a way that has nothing to do with the rename, and that breaks the migration of every run currently parked at that step.

The cost of the alternative is one string literal per step. You are not writing thousands of workflows an hour, and what the literal buys is that every step is migratable by construction — there is no such thing as a run parked somewhere a new version cannot find.

Two steps may not share a label#

A label is an identity, and two things under one identity is not an identity:

await saga.Run("leg", outbound);
await saga.Run("leg", inbound);     // compile error

workflow 'BookingSaga': two steps here are both labelled "leg", so a resumed run could not tell which of them it had already finished. Give them different labels.

Uniqueness is checked across the whole workflow — its start body, every state's enter body, and every route — because a run can be parked at any of them.

Why that error is at compile time#

This explains why you are asked now rather than never.

Two indistinguishable steps are only a problem when a parked run meets a new version — at deploy time, possibly weeks later. Reporting it then would be useless: the run parked with the duplicate already in place, so relabelling afterwards cannot help that run. It would be a complaint nobody could act on, repeating on every deploy for as long as the run lived.

Asked at compile time it is the opposite: you have not deployed, no run exists, and typing two names fixes it permanently.

Examples#

A booking saga with two legs of the same kind — an outbound flight and a return. Both run FlightFlow, so the labels are the only thing distinguishing them, and they are what let a run parked on the return leg still be recognised as "outbound done, return in progress" after a redeploy.

enum BookStatus { Start, Booked, Failed }
enum LegStatus  { Waiting, Done, Bad }

entity Booking {
  [Required, MaxLength(20)] string Ref;
  BookStatus Status = BookStatus.Start;
}

entity FlightLeg {
  [Required] Booking Booking;
  [Required, MaxLength(10)] string Direction;
  LegStatus Status = LegStatus.Waiting;
  bool Cancelled;
}

// The compensation. `Status` belongs to FlightFlow (it is what the workflow `Tracks`), so app code cannot assign it —
// a compensation records its own outcome on a field it owns.
void CancelFlight(FlightLeg leg) { leg.Cancelled = true; }

workflow FlightFlow {
  Tracks = FlightLeg.Status; Autostart = false; Initial = Waiting;
  event Finish();
  state Waiting { subscribe Finish(); on Finish { goto Done; } }
  terminal success Done { }
  terminal error   Bad  { Message = "the leg failed"; }
}
workflow BookingSaga {
  Tracks = Booking.Status; Autostart = false; Initial = Start;
  state Start {
    enter {
      var saga = Workflow.BeginSaga();
      try {
        var outbound = Workflow.Once("make-outbound", () => new FlightLeg { Booking = this.Item, Direction = "out" });
        await saga.Run("outbound-flight", outbound, () => CancelFlight(outbound));

        var inbound = Workflow.Once("make-inbound", () => new FlightLeg { Booking = this.Item, Direction = "back" });
        await saga.Run("return-flight", inbound, () => CancelFlight(inbound));

        saga.Complete();
        goto Booked;
      }
      catch (WorkflowError) { goto Failed; }
      finally { await saga.DisposeUnwind(); }
    }
  }
  terminal success Booked { }
  terminal error   Failed { Message = "the booking failed"; }
}

Notes#

  • A label names a step in the body, not a child run. Two different runs of the same workflow each have their own step at that label; the label distinguishes places in the code, not instances.
  • Labels are compared exactly, case included.
  • A fire-and-forget Workflow.Run(order) — one written without await — takes no label. Awaiting is what makes a start a park point, and only a park point has an identity a later version must match; a start you do not wait on has nothing to re-find. saga.Run always takes one, because every leg is joined.
  • The same reasoning, for a different mechanism, gives Workflow.Once (run a step at most once) its step label. A Workflow.Once label keeps an idempotency key stable across a deploy; a step label keeps a position recognisable across one.

See also#

Related

Workflow.BeginSaga (a compensating saga scope)

Open a saga scope that couples each forward step with its compensation. `await saga.Run("step", step, () => Undo(…))`…

Workflow.Run (start a workflow)

Start the workflow bound to an entity's type, on that entity. Bare — `Workflow.Run(order)` — is fire-and-forget: start…

Workflow.Once (run a step at most once)

Run something at most once per workflow run, however many times the surrounding code re-executes. The first execution…

Migrating runs that are still in flight

A deploy that renames or removes a workflow state leaves runs parked in it with nowhere to stand. A workflow migration…