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

Reference / Workflow

Remind (milestone reminders)

Remind <Name>(After = <TimeSpan>) { <body> } · Remind <Name>(After = <TimeSpan>, ThenEvery = <TimeSpan>) { <body> }

A reminder scheduled off a milestone. Its SCHEDULE is config in the header parens — `After` is the first fire (once, at `enter + After`), and the optional `ThenEvery` repeats it every interval. Its BODY is the block. The bare name `Within` in the schedule reads the enclosing milestone's `Within`, so `After = Within / 2` nudges at the halfway mark.

stable1 example compiled by CIworkflowauthoring

Summary#

A Remind schedules a side-effect off a Assigned / Finished (milestones) — a nudge that runs while the slot is still waiting, without transitioning the run. Its schedule is config, written in the header parentheses; its body is the block. After (required) is the first fire, at enter + After. ThenEvery (optional) repeats the body every interval after that. Reading the header tells you exactly when it fires — there is no hidden anchor. A reminder body may not goto — reminders are config, not transitions.

Signature#

Remind Nudge(After = <TimeSpan>) { <body> }                      // one-shot, at enter + After
Remind Chase(After = <TimeSpan>, ThenEvery = <TimeSpan>) { <body> }  // then repeats every interval

ThenEvery requires an After (it repeats the first fire). The header is config only; the { } block is the body. The old single-brace form (Remind { After = …; <statements> }) is not valid — config and body live in separate places.

The NAME is required, and it is the reminder's identity: Remind Nudge(After = …). A milestone may carry more than one reminder, so an unnamed Remind(…) is refused rather than guessed at — the parser says so by name.

Description#

A reminder lives inside a milestone (Assigned { … } / Finished { … }) and shares its ambient slot, so its body can read slot.Assignee / slot.Candidates and call your own app functions to act on them (e.g. a Notify(user) that sends over a client, or a foreach (var u in slot.Candidates) Notify(u)). Each firing writes a Reminded event to the timeline (readable via For(entity).Audit), which is the observable artifact a test asserts on.

The schedule reads as one timeline:

  • After = T — the first (and, alone, only) fire, at enter + T. After = Within / 2 reads the enclosing milestone's Within (the bare name Within in a reminder schedule inlines the milestone's Within expression) and nudges at the halfway mark.
  • ThenEvery = T — repeats the body every T after the first fire (After, After + T, After + 2T, …). A poll from the start is just After = T, ThenEvery = T.

A repeating reminder stops when its milestone resolves — Assigned once the slot leaves Unassigned, Finished once it is Satisfied, or when the milestone breaches (Unassigned/Unfinished fires). So the reminder nudges before the deadline while the breach handler owns the deadline itself; they never overlap and a reminder never nags forever.

A missed cadence is COALESCED, not replayed#

If the clock passes several due firings before the engine next runs — a scheduler that was down, a long jump in a test — the reminder fires once on the next advance, not once per interval it slept through. A ThenEvery = 2d reminder that goes unattended for a month sends one message, not fifteen.

This is what you want in production: coming back from an outage should not deliver a month of backlog to a customer. It has one consequence worth knowing, and it is easy to read as a bug in the reminder:

A test has to tick the way a scheduler does. Advancing the clock 30 days and settling once proves that the reminder still fires, not that it repeats. To assert a cadence, advance and settle per interval.

// asserts that it REPEATS — three separate advances, three settles
TestClock.Advance(TimeSpan.FromDays(3));  Workflow.Settle(ticket);   // After = 3d
TestClock.Advance(TimeSpan.FromDays(2));  Workflow.Settle(ticket);   // ThenEvery = 2d
TestClock.Advance(TimeSpan.FromDays(2));  Workflow.Settle(ticket);

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 ReminderFlow {
  Tracks    = Order.Status;
  Autostart = true;
  Initial   = Review;

  event Approve(Decision decision);

  state Review {
    subscribe Approve(Decision decision) as Legal {
      Candidates = u => u.Email != "";
      Finished {
        Within = TimeSpan.FromHours(8);
        // The header is config; the { } is the body. A reminder is NAMED — see the signature above.
        Remind Nudge(After = TimeSpan.FromHours(2)) { }
        Unfinished { }
      }
    }
    on Approve(Decision decision) { goto Done; }
  }
  terminal success Done { }
}

Nudge the pool halfway through the window, then chase hourly until someone picks it up or the breach fires:

Assigned {
  Within = TimeSpan.FromHours(4);
  Remind Nudge(After = Within / 2, ThenEvery = TimeSpan.FromHours(1)) {   // 2h, then 3h, 4h, …
    foreach (var u in slot.Candidates) { Notify(u); }
  }
  Unassigned { … }   // the deadline is owned here, separately
}

Drive it in a test and assert on the timeline, not the delivery:

TestClock.Advance(TimeSpan.FromHours(2));              // Remind Nudge(After = Within / 2, …) first fire
Workflow.Settle(po);

var audit = PoApproval.For(po).Audit;
Assert.Equal(1, audit.Count(a => a.Kind == AuditKind.Reminded && a.Slot == "Legal"));
Assert.True(PoApproval.For(po).Legal.IsUnassigned);    // …and we are STILL WAITING

See also#

Related

Assigned / Finished (milestones)

A milestone puts an SLA on a slot's progress — Assigned (someone must PICK IT UP within Within) and Finished (it must…

subscribe

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