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

Reference / Workflow

Assigned / Finished (milestones)

Assigned { Within = …; Unassigned { … } } · Finished { Within = …; Unfinished { … goto … } }

A milestone puts an SLA on a slot's progress — Assigned (someone must PICK IT UP within Within) and Finished (it must be SATISFIED within Within). Each carries a breach arm — Unassigned / Unfinished — that runs when the SLA lapses, and inside that arm the ambient `slot` is the slot the milestone hangs off: `slot.Assign(p)`, `slot.Candidates(u)`, `slot.Assignee`.

stable1 example compiled by CIworkflowauthoring

Summary#

A milestone puts a deadline on a slot's progress. There are two, by the two things that can be late: Assigned — the slot must be picked up (claimed or pre-assigned) within Within — and Finished — the slot must be satisfied (deposited into) within Within. Each has a breach arm that runs when its SLA lapses: Unassigned { … } for Assigned, Unfinished { … } for Finished. A breach is not a failure by itself: an arm with no goto runs its body and the wait lives on; an arm that gotos ends the wait.

Signature#

Assigned {
  Within = <TimeSpan>;         // must be picked up within this
  Unassigned { <body> }        // ran if still Unassigned at Within (may `goto`)
}
Finished {
  Within = <TimeSpan>;         // must be satisfied within this
  Unfinished { <body> }        // ran if not Satisfied at Within (may `goto`)
}

Description#

A milestone is declared where its SLA belongs — on a slot (inside its subscribe … { }), on a state, or on the workflow as a cascade default; nearest declaration wins (slot over state over workflow). A cascade default is inherited only by pool slots — a slot that declares Candidates — since the SLA models pool pickup (Assigned) and completion (Finished) and its body reads slot.Candidates; a bare system-event slot (subscribe Submit();) is not SLA-tracked. A slot may override just one clause (e.g. Finished { Within = TimeSpan.FromDays(2); }) and inherit the rest. Within is a TimeSpan expression over this.Item, so different instances can carry different SLAs (snapshot them in Start { }).

When the SLA lapses the milestone breaches: its breach arm runs, a Breached event is written to the timeline, and — if the arm gotos — the run transitions. A milestone that has already been met never breaches (an Assigned milestone is met the moment the slot leaves Unassigned; a Finished milestone the moment the slot is Satisfied), so a breach that fixes the problem (e.g. slot.Assign(lead)) also stops it recurring.

The ambient slot. Inside a milestone body the bare identifier slot is the slot the milestone hangs off:

  • slot.Assign(principal) — assign the slot to a principal (it becomes theirs; the wait continues).
  • slot.Candidates(u) — evaluate the slot's own Candidates predicate against a principal u → a bool. It is the slot's eligibility rule, reusable in a query — Person.Single(u => slot.Candidates(u) && u.IsLead) finds the lead among the slot's candidates as one query.
  • slot.Assignee / slot.Status / slot.IsUnassigned — read the slot's current holder / state.

Naming it. A breach arm may give the slot a name of its own — Unassigned(Slot approver) { … } — and then that name is the slot throughout the body. It is the same one slot either way; naming it just reads better when the body is about a person rather than a mechanism, and it matches how on Expire(PoStatus state) names what is ambient there. The parameter is a Slot and there is exactly one of it.

Assigned {
  Within = TimeSpan.FromHours(4);
  Unassigned(Slot approver) {                 // `approver` IS the slot — `slot` is simply its default name
    var lead = Person.Single(u => approver.Candidates(u) && u.IsLead);
    approver.Assign(lead);
  }
}

Retries / Backoff / Exhausted — try again before giving up#

For a slot a MACHINE fills — a child workflow, an external callback — the useful answer to a missed deadline is often "try again", not "escalate":

Finished {
  Within  = TimeSpan.FromMinutes(5);
  Retries = 3;                          // three further windows
  Backoff = TimeSpan.FromMinutes(1);    // …each one a minute after the last failed
  Exhausted  { slot.Release(); }        // tried and gave up — hand it to the humans
  Unfinished { goto NeedsAttention; }   // …and this is where the run goes
}

Each retry re-opens the SAME window (Within), delayed by Backoff. When the attempts run out, Exhausted { } runs and then the breach arm decides where the run goes.

  • A retry does not run the breach arm. The breach arm may goto, so running it per attempt would move the run away on the first one and there would be no second attempt. The breach arm is the end of the story, not a step in it.
  • Every attempt is audited, so the timeline shows three breaches and an exhaustion rather than one long silence.
  • The count is per owner. A slot that changes hands gives its new holder a fresh set of attempts, for the same reason they get a fresh budget: attempts burned by someone else are not a commitment they made.
  • On a HUMAN slot you almost certainly want Unassigned { … } instead. It lets you say who to escalate to and when to give up; Retries = N can only re-offer to the same pool on the same terms. It is allowed — three re-offers is odd, not wrong — but it is rarely what you meant.

Backoff takes either a plain time span — the same wait every time — or a retry policy, which says how the wait GROWS and bounds it:

Finished {
  Within  = TimeSpan.FromMinutes(5);
  // Three attempts, five minutes apart, then ten — but never more than an hour idle.
  Backoff = Backoff.Exponential(TimeSpan.FromMinutes(5)).MaxAttempts(3).Cap(TimeSpan.FromHours(1));
  Exhausted  { slot.Release(); }
  Unfinished { goto NeedsAttention; }
}

Retries = N and a policy's .MaxAttempts(M) are two budgets for one thing, and a milestone declaring both is a compile error. They also count differently — Retries is how many windows follow the first, MaxAttempts is how many there are in total — so Retries = 2 and .MaxAttempts(3) say the same thing. Pick whichever reads better where you are; see Backoff (retry policy).

enter { } — the success counterpart of the breach arm#

A milestone has two outcomes and both have a home. The breach arm runs when the SLA is missed; enter { } runs when the milestone is REACHED — the slot got an owner (Assigned), or it was satisfied (Finished).

Assigned {
  Within = TimeSpan.FromHours(4);
  enter  { Notify(slot.Assignee); }        // it has an owner now — tell them
  Unassigned { … }                          // …and this is what happens if it never did
}

This is the only home for "tell the new owner", and that is why it exists: claiming a slot changes no workflow state, so no state enter { } fires. Without it, an approval app has nowhere to put the most ordinary thing it does.

  • It runs on EVERY assignment, not just the first. A slot released and re-claimed enters Assigned again, and the new owner is told — the same reasoning that gives each holder their own Finished budget. A hook that fired once would go quiet exactly when a slot changes hands, which is when someone most needs to hear.
  • The ambient slot is bound, so slot.Assignee is the person who just took it.
  • It may not goto. Reaching a milestone is orthogonal to workflow state: assignment is about WHO, not about WHERE the run is. Side effects only — the compiler refuses a goto here.
  • It is available on Finished too (fires when the slot is satisfied), though that is often already covered by the on <Event> arm that handled the deposit.

Examples#

enum Decision   { Approve, Reject }
enum OrderState { Review, Done }

[Principal]
entity Person {
  [Required, MaxLength(200)] string Email;
  security { allow read, create when IsAuthenticated; }
}

entity Order {
  [Required, MaxLength(60)] string Reference;
  [Required] Person Requester;
  OrderState Status;                       // no default: the workflow owns this field
  security { allow read, create, update when IsAuthenticated; }
}

workflow ReviewFlow {
  Tracks    = Order.Status;
  Autostart = true;
  Initial   = Review;

  event Approve(Decision decision);

  state Review {
    subscribe Approve(Decision decision) as Legal {
      Candidates = u => u.Email != "";
      Assigned {
        Within = TimeSpan.FromHours(4);
        Unassigned { }
      }
    }
    on Approve(Decision decision) { goto Done; }
  }
  terminal success Done { }
}

Breach ≠ failure — nobody picked up the pool slot, so on breach we widen it to the department lead and keep waiting:

subscribe Approve(Decision decision, string reason) as Legal {
  Candidates = u => u.Department == Dept.Legal;
  Assigned {
    Within = TimeSpan.FromHours(4);
    Unassigned {                                   // no goto → keep waiting, now with an owner
      var lead = Person.Single(u => slot.Candidates(u) && u.IsLead);
      slot.Assign(lead);
    }
  }
}

A claimed slot that goes overdue is fatal — the goto ends the wait:

Finished {
  Within = TimeSpan.FromHours(8);
  Unfinished { goto Escalated; }
}

See also#

  • subscribe — the slot a milestone hangs off (its Candidates / Assignee)
  • Candidates (slot) — the same Candidates(u) question OUTSIDE a milestone, where the run is named rather than ambient: <Wf>.For(item).<Slot>.Candidates(u)
  • Remind (milestone reminders) — nudges scheduled off a milestone before it breaches
  • <span class="planned" title="this page is planned and not written yet">workflow-route</span> — the state-level on … goto … routes a breach goto joins
  • <span class="planned" title="this page is planned and not written yet">workflow-state</span> — the enclosing state and its Expire deadline

Related

subscribe

Declares that a workflow state waits on an event, and configures the wait — who may hold it, who may hand it on…

Remind (milestone reminders)

A reminder scheduled off a milestone. Its SCHEDULE is config in the header parens — `After` is the first fire (once, at…

Backoff (retry policy)

A retry policy as a value: how long to wait before the next attempt. Three shapes say how the wait GROWS — `Fixed` (the…