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

Reference / Workflow

Assign — handing a slot to a named colleague

Workflow.Assign(row, principal) · <Wf>.For(item).<Slot>.Assign(principal) · Reassign = actor => predicate;

Give a slot to somebody else. Claiming takes work for yourself and releasing puts it back in the pool; assigning is the third move — a hand-over. It answers two questions where a claim answers one: may the actor move this slot, and may the target hold it. The second is the slot's own `Candidates`; the first is `Reassign`, and without it the answer is the holder alone.

stable1 example compiled by CIworkflowauthoringsecurity

Summary#

Assign hands a slot to a named person. Claim takes one for the caller and Release puts it back in the pool; neither can express "this is Bob's now".

The whole design is in one asymmetry. A claim asks one question — does the actor satisfy Candidates — because the actor and the new holder are the same person. An assign separates them, so it asks two:

questionasked ofanswered by
may this slot be moved by you?the actorthe holder rule, plus Reassign
may it be held by them?the targetCandidates, exactly as a claim

Signature#

Workflow.Assign(row, principal)                 // from a queue row
<Wf>.For(item).<Slot>.Assign(principal)          // from a page that already names the run

…and the declaration that says who besides the holder may do it:

subscribe Respond() as Reply {
  Candidates = u => u.Team == Team.Support;      // who may HOLD it
  Reassign   = a => a.IsSupervisor;              // who may MOVE it
}

Description#

Absent Reassign means the holder, and nobody else#

A slot that declares no Reassign can be handed on by its current holder alone. Not by a supervisor, not by somebody who could have claimed it themselves — and, because an unheld slot has no holder, not by anyone at all until the app says who may.

That is deliberate and it is the same posture as the rest of the security model: a slot is a commitment somebody made, so widening who can move it is a decision an app states rather than one it inherits. Being eligible to hold work is not authority over work — which is why Candidates cannot stand in for this. A supervisor who satisfies Candidates is refused on a slot that names nobody, and that case is worth internalising: it is exactly where treating the two questions as one would let the wrong person through.

The target meets the same gate a claimer would#

Candidates is evaluated against the person receiving the slot. If they could not have claimed it, it cannot be assigned to them. Otherwise an assign would be the way around the pool's own rule, and every Candidates in the app would be advisory.

An assign is not a claim#

The row records which it was. Assignee moves to the new holder, and ClaimedBy/ClaimedAt are cleared — they record who took the slot, and the new holder did not. Leaving the previous name there would read as a hold that person no longer has.

The hand-over itself is on the audit trail as an Assigned event, whose actor is whoever moved it and whose PreviousAssignee is who lost it.

The clocks behave exactly as they do on a claim#

  • the Finished promise restarts for the new owner — a budget somebody had no chance to meet is a hot potato, not a commitment;
  • the Assigned { enter { } } hook fires, so "tell the new owner" works for a hand-over and not only for a pickup;
  • AssignmentCount goes up one, which is what makes churn countable without walking the trail.

Expire and Deadline never restart. They are the absolute caps, and they are what bounds passing work around.

Assigning to the current holder does nothing — no clock restart, no audit row. A fresh budget for a move that did not happen would be the same hot potato by another route.

Which of the two forms do I write?#

Both do the same thing and meet the same gates.

  • Workflow.Assign(row, principal) — from a queue read, where the row already names the run and the slot. This is the form a board or hand-over screen wants.
  • <Wf>.For(item).<Slot>.Assign(principal) — from a page that knows the item and names the slot statically.

⚠ On a dynamically fanned-out slot the static form addresses the actor's own instance"hand mine on" — because that is how an alias resolves for the acting principal. To move somebody else's instance, use the row form, which names the one slot it is a row for.

What is refused, and when#

the compiler catchesthe run catches
the target is not the app's [Principal] typethe actor may not move this slot
the target does not satisfy Candidates
the slot is already satisfied, cancelled or breached
the slot has not opened yet (its After predecessors are unsatisfied)

Every refusal writes a Refused row to the trail before it throws, so a rejected hand-over is visible rather than merely unsuccessful.

Examples#

A support desk where the reply slot may be moved by a supervisor and the park slot may not:

enum Stage { Working, Done }

[Principal] entity Agent {
  [Required, MaxLength(80)] string Name;
  bool IsSupervisor;
  bool OnDuty;
  security { allow read, create when IsAuthenticated; }
}

entity Ticket {
  [Required, MaxLength(120)] string Subject;
  Stage State;
  security { allow read, create, update when IsAuthenticated; }
}

workflow TicketFlow {
  Tracks    = Ticket.State;
  Autostart = true;
  Initial   = Working;

  event Fix();
  event Park();

  state Working {
    // A supervisor may move this one even though they do not hold it.
    subscribe Fix() as FixIt {
      Candidates = u => u.OnDuty;
      Reassign   = a => a.IsSupervisor;
    }
    // …and this one names nobody, so it is the holder's alone to hand on.
    subscribe Park() as Parked {
      Candidates = u => u.OnDuty;
    }
    on Complete { goto Done; }
  }

  terminal success Done { }
}

// The hand-over button on a queue screen: the row is already in hand, so the slot needs no naming.
void HandOver(Ticket ticket, Agent to) {
  var row = Workflow.Work<Ticket>()
                    .Where(r => r.SlotAlias == "FixIt" && r.Item == ticket)
                    .First();
  Workflow.Assign(row, to);
}

// The same move from a page that names the slot itself.
void HandOverParked(Ticket ticket, Agent to) {
  TicketFlow.For(ticket).Parked.Assign(to);
}

Notes#

A live read over the run wakes on it. An assign changes no property on the item, so it signals the item's change channel deliberately — otherwise a page's moves, timeline and checklist would go on showing the state from before the hand-over until something else happened to the row.

An assign needs an acting principal. Anonymous is nobody the app can have admitted, so there is nothing for the authority gate to answer. Inside a test, that is runas.

Inside a milestone body, slot.Assign(p) is a different thing. A breach escalation or a follow-the-sun reminder already runs as the app's own code with no acting principal, so no authority question arises there — the app is not being asked whether it may move its own slot. The clock restart, the count and the audit row are the same.

Who a screen may offer the button to is <Wf>.For(item).<Slot>.Candidates(u) — the same predicate the engine evaluates, so a picker does not re-type the rule and drift from it.

See also#

Related

Candidates (slot)

Declares WHO may hold or satisfy a `subscribe` slot. `Candidates` is one expression surface that dispatches on its…

Acting on an inbox row (deposit, claim, release)

Answer a queued slot from the row itself. The event is named at the call site because a queue's rows are heterogeneous…

Workflow.Work&lt;T&gt; (everything outstanding) and its SLA numbers

Every live slot of every run tracking T, whoever holds it — the unfiltered sibling of Workflow.Inbox. Rows carry…

subscribe

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

Transitions — where this item may go next

One row per move this instance can make right now, with each arm's guard evaluated against it. A board offers only the…

For(entity).Audit

Reads a running instance's lifecycle timeline — every transition, claim, deposit, reminder and refusal as an…