# enter and exit (a state's arrival and departure hooks)

> Run code when a run arrives in a state, and when it leaves. enter may redirect the run somewhere else; exit may not, because by the time it runs the move has already been decided. Both are optional and neither changes when the state's deadline clock counts.

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

## Summary        {#summary}
A state can run code at two moments: when a run **arrives** in it, and when a run **leaves** it.

```osy syntax
state UnderReview {
  enter { this.Item.ReviewStartedAt = DateTime.UtcNow; }
  exit  { this.Item.ReviewMinutes = (DateTime.UtcNow - this.Item.ReviewStartedAt).TotalMinutes; }
}
```

Both are optional, and both run inside the same transaction as the move that triggered them — so if either throws,
the move does not happen at all.

## Signature      {#signature}
```osy syntax
state <Name> {
  enter { <statements> }    // on arrival — MAY `goto` to redirect the run
  exit  { <statements> }    // on departure — may NOT `goto`
}
```

## Description    {#description}

### Can `enter`/`exit` redirect with `goto`?     {#goto}
`enter` may redirect: `enter { if (order.Total > 10000) goto NeedsApproval; }` sends the run somewhere else instead of
settling in this state. That works because on arrival, **where the run ends up is still the thing being decided**.

`exit` may not, and a `goto` in one is a compile error. By the time an exit body runs the transition has **already**
been decided — something chose the destination, and this body is running *because* of that choice. A redirect here
would either silently override a decision already made, or, if two states' exit bodies each redirected into the
other, bounce between them for ever with no arm to break the cycle.

The refusal names where the decision does belong:

```text
an `exit { }` body may not `goto` — state 'Open' runs its exit body BECAUSE a transition was already decided, so a
`goto` here would override a choice already made (and two states redirecting to each other would never settle). Put
the decision where it is still open: an `on` arm, a `complete when`, or the destination state's own `enter { }`,
which MAY redirect.
```

It reads the same way as the rule for milestones, which likewise may not `goto` from their `enter`: **a body that runs
as a consequence of a decision does not get to re-make it.**

### In what order do `exit`, `enter` and the deadline run?     {#order}
Leaving `A` for `B` runs, in one transaction:

1. `A`'s `exit { }`
2. `B`'s `enter { }`
3. `B`'s waits and deadline are armed

So an exit body sees the world as it was in the state it is leaving, before anything about the destination applies.

### Deadlines and `Accrues`     {#clocks}
A state's SLA clock has already **stopped** by the time its exit body runs. `Accrues` measures time spent *in* the
state, and work done on the way out is not that — so an exit body can never inflate the very measurement it is often
there to record.

### What happens if a body reaches the network?     {#network}
Wrap it. A body here is not an ordinary function: **nobody called it**, so there is nobody to hand a failure to.

A timeout, a DNS failure or a refused connection is not a workflow outcome — the platform treats it as worth another
attempt, so the delivery is retried and **this whole body runs again from the top**, with the row still sitting in the
state it was arriving in. That is right for a blip and wrong for an endpoint that is simply gone, and either way it is
not what you would have chosen if you had been asked.

`osy lint` asks for you, in every body a workflow declares — `enter`, `exit`, an `on` route, a milestone arm, a
`Remind`. Three findings cover the two ways a call can go wrong, and they cover the reach **through a helper** as well
as the call written here, because a body that says only `SendMail(this.Item)` warns you of nothing in its own text:

| finding | what it saw |
|---|---|
| `reliability-outbound-call-unguarded` | an outbound call written in this body, with no `try` |
| `reliability-reaches-the-network-unguarded` | this body reaches the network **through something it calls**, with no `try` |
| `reliability-http-result-unchecked` | an `Http.*` result used without asking whether the call worked — a 404 is a normal return and no `try` can catch it |

A helper that catches everything itself counts as handled, however many hops down it is, so you write the guard once
where it means something rather than at every level.

```osy title="a state that mails on arrival — the guard is the difference between a nudge and a stuck run" test app=workflow-enter-exit
class MailRequest { public string To; public string Subject; }
class MailResult  { public string Id; }

client Mailer {
  BaseUrl = "https://api.mail.example";
  [Post("/send")] MailResult Send(MailRequest body);
}

entity Invitation {
  [Required, MaxLength(200)] string Email;
  InviteStatus Status;   // no default: `InviteFlow` autostarts, so its `Initial` IS this field's first value
}
enum InviteStatus { Pending, Accepted }

// The guard lives HERE, in the one place that knows what a failed send means: the invitation is still open, the run
// still moves on, and the failure is findable. Without it the send is retried with everything above it.
void SendInvite(Invitation inv) {
  try {
    var sent = Mailer.Send(new MailRequest { To = inv.Email, Subject = "You are invited" });
  }
  catch (Exception e) {
    Log.Error(e, "invitation mail failed for {Email}", inv.Email);
  }
}

workflow InviteFlow {
  Tracks    = Invitation.Status;
  Autostart = true;
  Initial   = Pending;

  event Accept();

  state Pending {
    enter { SendInvite(this.Item); }
    subscribe Accept() as Acceptance;
    on Acceptance { goto Accepted; }
  }

  terminal success Accepted { }
}
```

### Do `enter`/`exit` run on a terminal state?     {#terminals}
A terminal state can declare an `enter { }`, which runs as the run finishes. It can declare an `exit { }` too, but
nothing will ever run it: a terminal is where runs stop. Prefer putting the work in `enter`.

## Examples       {#examples}

Recording how long a review took — the pair working together, which is what `exit` exists for:

```osy title="timing-a-state" test app=workflow-enter-exit
entity Review {
  [Required, MaxLength(200)] string Title;
  DateTime? StartedAt;
  int Minutes;
  ReviewStatus Status = ReviewStatus.Pending;
}
enum ReviewStatus { Pending, UnderReview, Done }

workflow ReviewFlow {
  Tracks    = Review.Status;
  Autostart = false;
  Initial   = Pending;

  event Begin();
  event Finish();

  state Pending {
    subscribe Begin() as B;
    on B { goto UnderReview; }
  }

  state UnderReview {
    enter { this.Item.StartedAt = DateTime.UtcNow; }
    // Runs on the way out, whichever arm caused the move — so a second way out of this state cannot forget it.
    exit  { this.Item.Minutes = this.Item.Minutes + 1; }

    subscribe Finish() as F;
    on F { goto Done; }
  }

  terminal success Done { }
}
```

That last point is the practical argument for `exit` over repeating the line at the end of every arm: a state with
three ways out needs the bookkeeping written once, and a fourth arm added later cannot silently omit it.

## See also       {#see-also}
- [complete when (a state's own completion condition)](https://osysharp.com/reference/workflow/complete-when/) — ending a state on a condition rather than on a particular event
- [Assigned / Finished (milestones)](https://osysharp.com/reference/workflow/milestone/) — `Assigned`/`Finished` timers, whose own `enter` may not `goto` either
- [ServiceHours (SLA-accrual windows)](https://osysharp.com/reference/workflow/service-hours/) — what `Accrues` measures, and therefore what an exit body is outside of
- [Tracks and Initial (the field a workflow drives)](https://osysharp.com/reference/workflow/tracks/) — the enum whose members these states are
- [a typed HTTP client (client)](https://osysharp.com/reference/http/client/) — declaring the typed client a body like the one above calls
- [When a child is cancelled or fails](https://osysharp.com/reference/workflow/fault-propagation/) — what an uncaught *workflow* outcome does, which is not what a network fault does
