# Remind (milestone reminders)

> 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. A reminder on the run's `Deadline` is measured on the deadline's own clock — at risk before a resolution breaches.

<!-- id: workflow-remind · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/remind/ -->

## Summary        {#summary}
A **`Remind`** schedules a side-effect off a [Assigned / Finished (milestones)](https://osysharp.com/reference/workflow/milestone/) — 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      {#signature}
```osy syntax
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    {#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](https://osysharp.com/reference/workflow/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   {#catch-up}

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.

```osy syntax
// 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);
```

### On the run's whole deadline   {#deadline}
A milestone belongs to one slot in one state, and its clock starts over when the state is entered again. The run's
`Deadline` is different: it spans the whole run, accruing in the states named by `Accrues` and pausing in the rest. A
reminder written inside it is measured on **that** clock:

```osy syntax
Deadline {
  Within = this.Item.ResolveWithin;
  Remind AtRisk(After = Within * 0.75) { Warn(this.Item); }   // at three quarters of the time ACCRUED
}
```

- It fires when the deadline has **accrued** `After` — so a run that sat for a week in a state outside `Accrues` is
  reminded at the same point in its promise as one that never waited.
- `Within` is the deadline's own budget, as it is a milestone's.
- When the deadline is **retargeted** (`Workflow.Retarget()`), the reminder moves with it, keeping the time already
  accrued: "at three quarters" stays three quarters of the new promise. A slot milestone's reminders move the same way
  ([Workflow.Retarget (re-base the SLA clocks)](https://osysharp.com/reference/workflow/retarget/)); one that has already fired is left alone.
- There is no slot, so its body has no ambient `slot`. It fires once (or every `ThenEvery`), never after the run ends.

### Before a known instant — an expiry, a start time   {#before-an-instant}

A reminder is scheduled from when its clock started, so "a week before the lease ends" is a run whose deadline IS
that instant, with the reminder a week short of it. `Within` may read the row the run tracks: measured from the moment
the run began, the instant is `this.Item.EndsAt - this.Item.SignedAt`, and the reminder fires at `Within -
TimeSpan.FromDays(7)`. See the last example below; [TestClock.Advance and TestClock.Set](https://osysharp.com/reference/testing/clock-advance/) drives it in a test.

### A reminder that moves its own run   {#raise-own-run}

A poll is a reminder whose body asks something and, on the right answer, raises the run's own event:
`Remind Track(After = …, ThenEvery = …) { if (Delivered(this.Item)) { Flow.RaiseArrive(this.Item); } }`. The raise is
delivered as soon as the reminder's step ends, in the same `Workflow.Settle` — see [[workflow-raise#own-run]]. The
reminder itself may not `goto`; raising is how it hands the decision to the state's own arm.

## Examples       {#examples}
```osy title="nudge the holder before the deadline" test app=workflow-remind
using Osysharp.Workflow;

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:

A resolution promise that warns at three quarters of its accrued time, before it breaches:

```osy title="at risk on the whole-run deadline" test app=workflow-remind-deadline
using Osysharp.Workflow;

enum CaseState { Open, Waiting, Closed }

entity Case {
  [Required, MaxLength(200)] string Title;
  TimeSpan ResolveWithin = TimeSpan.FromHours(8);
  bool AtRisk = false;
  CaseState Status;                        // no default: the workflow owns this field
  security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}

workflow CaseFlow {
  Tracks    = Case.Status;
  Autostart = true;
  Initial   = Open;
  Accrues   = [Open];                      // waiting on the customer stops the clock — and the reminder with it

  Deadline {
    Within = this.Item.ResolveWithin;
    Remind AtRisk(After = Within * 0.75) { this.Item.AtRisk = true; }
  }

  event Wait();
  event Resume();
  event Close();
  state Open { on Wait { goto Waiting; } on Close { goto Closed; } }
  state Waiting { on Resume { goto Open; } }
  terminal success Closed { }
}
```

A week before a known instant — the deadline falls when the lease ends, and the reminder a week short of it:

```osy title="remind a week before a lease ends" test app=workflow-remind-before
using Osysharp.Workflow;

enum LeaseState { Running, Ended }

entity Lease {
  [Required, MaxLength(80)] string Tenant;
  DateTime SignedAt = DateTime.UtcNow;
  DateTime EndsAt;
  bool RenewalOffered = false;
  LeaseState Status;                       // no default: the workflow owns this field
  security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}

workflow LeaseFlow {
  Tracks    = Lease.Status;
  Autostart = true;
  Initial   = Running;

  // Measured from when the run began (the signing), the deadline falls when the lease ends.
  Deadline {
    Within = this.Item.EndsAt - this.Item.SignedAt;
    Remind WeekBefore(After = Within - TimeSpan.FromDays(7)) { this.Item.RenewalOffered = true; }
  }

  event End();
  state Running { on End { goto Ended; } }
  terminal success Ended { }
}
```

```osy title="nudge, then chase" syntax
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:

```osy title="a reminder survives — still waiting" syntax
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       {#see-also}
- [Assigned / Finished (milestones)](https://osysharp.com/reference/workflow/milestone/) — the SLA a reminder is scheduled off (and the source of `Within`)
- [For(entity).Audit](https://osysharp.com/reference/workflow/audit/) — the `Reminded` timeline event each firing writes
