# Tracks and Initial (the field a workflow drives)

> Names the enum field a workflow owns and the state a run starts in. No application code may write that field, and when the workflow autostarts it also supplies the field's starting value, so the entity declares no default.

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

## Summary        {#summary}
A workflow drives one field on one entity: the field that says where a row has got to. **`Tracks`** names that field
and **`Initial`** names the state a fresh run begins in.

Together they hand the field over. From that point it is the workflow's, not the application's — it moves only by
transition, and reading it anywhere tells you the truth about the run. When the workflow starts with the row, it
supplies the field's opening value too.

## Signature      {#signature}
```osy syntax
workflow <Name> {
  Tracks  = <Entity>.<Property>;
  Initial = <State>;
  …
}
```
The property is an enum-typed member of the tracked entity, and every state and terminal in the workflow must be a
member of that enum — so the field's type *is* the workflow's vocabulary of states.

## Description    {#description}
### When the workflow starts with the row, the field needs no default   {#autostart}
An `Autostart = true` workflow begins the moment its row exists, so `Initial` *is* the field's value from the start
and the entity does not restate it:

```osy title="the field is declared bare — the workflow supplies its starting value" test app=workflow-tracks
using Osysharp.Workflow;

enum ApprovalStage { Pending, Approved, Rejected }

entity Invoice {
  [Required] [MaxLength(120)] string Description;
  [Required] decimal Amount;
  ApprovalStage Stage;
  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

workflow ExpenseApproval {
  Tracks    = Invoice.Stage;
  Autostart = true;
  Initial   = Pending;

  event Decide(bool approved);

  state Pending {
    subscribe Decide(bool approved);
    on Decide(bool approved) {
      when (approved) { goto Approved; }
      default { goto Rejected; }
    }
  }

  terminal success Approved { }
  terminal error   Rejected { Message = "rejected"; }
}
```

A new `Invoice` reads back `Pending` with nothing in the source saying so twice.

This matters more than it looks. Elsewhere in the language a bare enum member is **required** — the platform will not
invent a value for it, because the first member of an enum is a position, not a decision, and reordering the members
would silently change what every new row means. A tracked field is the one case where a real value is genuinely
available: the workflow declared it.

### WHEN does it start, exactly?   {#autostart-timing}
"Begins the moment its row exists" is precise, not a loose way of saying "soon": an `Autostart = true` workflow's
initial `enter{}` runs **synchronously, inside the same commit** that created the row — before the next statement
after `UnitOfWork.Commit()` executes, in the SAME function, with no pump or wait of any kind. A read immediately
after that commit already sees whatever the `enter{}` body wrote:

```osy title="the enter body has already run by the time the NEXT function looks" run app=workflow-tracks-autostart-timing
using Osysharp.Workflow;

enum TicketState { Open, Closed }

entity Ticket {
  [MaxLength(80)] string Title;
  TicketState Status;
  security { allow read, create when IsAnonymous || IsAuthenticated; }
}

entity AuditEntry {
  [Required, MaxLength(80)] string Note;
  security { allow read, create when IsAnonymous || IsAuthenticated; }
}

workflow TicketFlow {
  Tracks = Ticket.Status;
  Autostart = true;
  Initial = Open;
  event Close();
  state Open {
    enter { new AuditEntry { Note = "opened" }; }
    on Close { goto Closed; }
  }
  terminal success Closed { }
}

[Test]
void AutostartedEnterBody_RunsInTheSameCommit_NoPumpNeeded() {
  new Ticket { Title = "t" };
  UnitOfWork.Commit();

  // No `Workflow.Settle(...)` here — the enter{} body has already run, in the commit above.
  Assert.Equal(1, AuditEntry.Count());
}
```

⚠ **This is a deliberate platform guarantee, not an implementation detail that happens to hold today.** In
production, autostart is ALSO delivered by a durable `wf-start` job the platform enqueues at commit — the backstop
for a row committed by a path that is not Osy# (a REST write, an import) and for recovering a start this process
died during. But an Osy# `UnitOfWork.Commit()` drives the autostart pass itself before returning, specifically so
"create a row, then ask about it" reads the way it looks. Starting the same run twice is a no-op by design, so the
durable job racing (or duplicating) the in-process start is harmless.

**`Workflow.Settle(entity)` is a TEST-ONLY primitive** for a different problem: draining DUE, ALREADY-SCHEDULED work
— timers, reminders, a milestone's deadline — deterministically, without a real clock to wait on. It has no
counterpart in production code, where that same work is drained by the durable dispatcher on its own schedule. Reach
for it in a test that needs to observe a timer fire or a deadline expire; an autostarted `enter{}` needs no `Settle`
at all, because it has already run by the time `UnitOfWork.Commit()` returns.

**A caller with no Osy# session can still trigger an autostart** — a webhook, a scheduled tick, a REST write — just
on the durable job's timing rather than in the same instant: the row lands, the write's own commit finishes (with no
run attached yet), and the durable `wf-start` job picks it up and starts the run shortly after. What is guaranteed
synchronous is specifically an Osy# `UnitOfWork.Commit()`; every other write path gets the same eventual guarantee,
on the dispatcher's cadence rather than in-line.

**One bound, and where it does not apply.** A served app's commit waits up to **5 seconds** for that first step.
An entry that takes longer — it calls a model, fetches a page, does real work — is handed to the durable `wf-start`
job and keeps running, and the commit returns without it: so a read on the next line can see the row from before its
run entered. **Under `osy test` there is no bound**: a fixture's seed and a test (and the server calls its clicks make)
always wait for the first step, so a busy machine cannot turn into a different test result — which also means no test
will show you the bound. `osy lint` does: `workflow-first-step-waits-on-a-slow-call` names an autostarted workflow
whose first step reaches the network or a model, directly or through anything it calls.

⚠ **The first step is more than the first `enter`.** It is the `Start` body, the initial state's `enter`, and the
`enter` of every state that `enter` sends the run to with `goto` — the engine takes those moves in the same call. So
moving a slow call into "the next state" behind a `goto` in `enter` does not take it out of the first step. What does
is a state the run **parks** in and leaves by a **route** the platform runs later — here a clock due at once:

```osy title="the first step decides and parks; the slow call happens when the clock fires" run app=workflow-tracks-quick-first-step
using Osysharp.Workflow;
using Osysharp.Http;

app Outbox {
  model "**/*.osy";
  use Osysharp.Http;
  use Osysharp.Workflow;
}

enum NoticeState { Queued, Sent, Held }

entity Notice {
  [MaxLength(200)] string To;
  NoticeState Status;
  security { allow read, create when IsAnonymous || IsAuthenticated; }
}

workflow NoticeDelivery {
  Tracks    = Notice.Status;
  Autostart = true;
  Initial   = Queued;

  state Queued {
    // Quick: only what can be decided here.
    enter { if (this.Item.To == "") { goto Held; } }
    // A clock due at once. Its route runs after the commit, so the commit never waits on the far end.
    Expire { Within = TimeSpan.Zero; Accrues = false; }
    on Expire {
      try {
        var r = Http.Post("https://notices.example/send", this.Item.To, "text/plain");
        if (!r.IsSuccess) { Log.Warning("notice to {To} refused: {Status}", this.Item.To, r.StatusCode); }
      }
      catch (Exception e) { Log.Error(e, "notice to {To} failed", this.Item.To); }
      goto Sent;
    }
  }

  terminal success Sent { }
  terminal cancel  Held { }
}

[Test]
void The_commit_returns_before_the_send_and_the_clock_makes_it() {
  Http.Post.Stub((url, body) => new HttpResponse { StatusCode = 202, Body = "" });
  var n = new Notice { To = "someone@example.com" };
  UnitOfWork.Commit();
  Assert.Equal(NoticeState.Queued, Notice.Where(x => x.Id == n.Id).First().Status);   // parked, nothing sent yet

  Workflow.Settle(n);   // in a test, due clocks fire when you settle; in production the dispatcher fires them
  Assert.Equal(NoticeState.Sent, Notice.Where(x => x.Id == n.Id).First().Status);
}
```

### The first `enter` cannot refuse the row   {#enter-cannot-refuse}
The row is committed **before** the run starts, so nothing an autostarted `enter{}` does can take the row back. A
`throw` in it does not fail the caller's `UnitOfWork.Commit()`: the row stays, the fault is logged as a warning
(`in-process autostart failed`), and the durable job retries the start later. If the first step is a DECISION that
can go either way — is the stock still there, is the slot still free — make it a state of its own that moves on
to one outcome or the other, and let the caller read which after its commit:

```osy title="a deciding first state — the caller reads the outcome on its next line" syntax
workflow OrderLife {
  Tracks = Order.Status;
  Autostart = true;
  Initial = Placed;

  state Placed {
    enter {
      if (this.Item.Lines.Any(l => l.Variant.Stock < l.Quantity)) {
        this.Item.Problem = "Some of it has just sold.";
        goto Unavailable;
      }
      foreach (var l in this.Item.Lines) { l.Variant.Stock = l.Variant.Stock - l.Quantity; }
      goto Reserved;
    }
  }
  state Reserved { }
  terminal cancel Unavailable { }
}

string Place(Order order) {
  UnitOfWork.Commit();
  return order.Status == OrderStatus.Reserved ? "Reserved." : order.Problem;
}
```

The body writes as the system ([What a workflow body may write](https://osysharp.com/reference/workflow/body-security/)), which is what lets an anonymous visitor's order take
stock the visitor could never write directly. Two such entries racing for the last unit meet at
[ConcurrencyCheck](https://osysharp.com/reference/entity/concurrency-check/): one commits and the other's start is retried, and the retry sees the stock as it now is.

### Restating it is refused   {#no-restate}
Writing the default anyway is a compile error, not a redundancy the compiler tolerates:

```osy title="refused — the workflow already supplies it" syntax
ApprovalStage Stage = ApprovalStage.Pending;   // 'Stage' on 'Invoice' is owned by workflow 'ExpenseApproval' …
```

The reason is drift. Two copies of one fact stay in step only while someone keeps them there, and the copy a reader
believes is the one in the entity — so the day `Initial` changes and the declaration does not, the source says
something false and nothing reports it. One statement of the fact cannot disagree with itself.

### The field is read-only to application code   {#read-only}
No function, action, or object initializer may assign a tracked field; a workflow's own handlers may. This is the
same ownership seen from the other side — if application code could set the field, the state would no longer mean
"where this run has got to", it would mean "whatever was written last".

```osy title="the state moves by transition, never by assignment" syntax
invoice.Stage = ApprovalStage.Approved;   // refused — the workflow drives it
```

To move a run, send it an event and let a route transition it.

A workflow's own handlers may assign the field, as above — **its own** field, on the row its run is bound to.
A handler of a *different* workflow may not: that field belongs to the other workflow, so the compiler refuses it
and names the owner. Raise the owner's event instead.

### Nothing else writes it either — the platform refuses it at commit   {#who-writes}
The compiler can only see the Osy# you compile. A tracked field is also guarded where every write lands, so the
rule holds for writers that are not your source at all:

- **A page's client, or anyone sending requests by hand.** Changing the field of an existing row through the data
  API is refused when the change is saved, even when the row's `security { }` lets that person update it. A
  customer allowed to edit their own order cannot move it to `Shipped` by sending `Status = Shipped`.
- **A new row cannot choose where it starts.** It may leave the field out (or carry its declared default); any other
  value is refused. Where a row starts is `Initial`'s decision.
- **Platform code with full privileges is no exception.** Running as the system is not being the workflow.
- **Bulk statements** — `.Update(…)` over a query, or an insert from a query — may not name the field at all,
  because they write rows no run is working on.

What *does* write it: the run bound to that row, when it enters a state (a start, a `goto`, a terminal); a handler
of that same run assigning it; and a version upgrade moving the run to its new version's state. The refusal names the
field, its workflow, and the event to raise instead:

```osy title="what a refused write says" syntax
'Stage' on 'Invoice' is owned by workflow 'ExpenseApproval' (Tracks = Invoice.Stage) — only that row's own run may
move it, and this write did not come from it. Raise one of 'ExpenseApproval''s events instead
(`ExpenseApproval.Raise<Event>(row, …)` or `Workflow.Raise(row, <Event>(…))`), and let its transition move the field.
```

So the field always means what it says: where that row's run has got to, and never whatever somebody wrote last.

### When the run starts later, the field's earlier value is yours to state   {#late-start}
`Initial` is where the **run** begins, not where the **row** begins. Those are the same moment only when the workflow
autostarts. If it does not — `Autostart = false`, or a condition that is not yet true — the row exists for a while
with no run attached, and what the field reads in that window is a fact only you know:

```osy title="a report is a draft before it is anyone else's problem" test app=workflow-tracks-draft
using Osysharp.Workflow;

enum ReportStage { Draft, InApproval, Approved, Rejected }

entity Report {
  [Required] [MaxLength(120)] string Title;
  DateTime? SubmittedAt;
  ReportStage Stage = ReportStage.Draft;
  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

workflow ReportApproval {
  Tracks    = Report.Stage;
  Autostart = this.Item.SubmittedAt != null;   // starts on submit, not on create
  Initial   = InApproval;

  event Decide(bool approved);

  state InApproval {
    subscribe Decide(bool approved);
    on Decide(bool approved) {
      when (approved) { goto Approved; }
      default { goto Rejected; }
    }
  }

  terminal success Approved { }
  terminal error   Rejected { Message = "rejected"; }
}
```

`Draft` is a member of the tracked enum but not a state the workflow ever occupies — it is where a report sits before
the workflow has anything to do. Here the default is required, and it is not refused: the workflow is not claiming to
supply it.

**So the question "does my tracked field need a default?" has one answer: does the workflow start when the row is
created?** If yes, the workflow supplies the value and you must not restate it. If no, it is yours.

## Examples       {#examples}
A starting state decided per row rather than fixed. `Initial` only ever *names* a state — `Initial = <expression>`
is refused — so the choice goes in the `Start` body, which runs before the run enters `Initial` and may `goto` any
state instead, with the whole row in scope. A `goto` names one state, so a choice among several is one branch each:

```osy title="a starting state that depends on the row" test app=workflow-tracks-start-per-row
using Osysharp.Workflow;

enum ApprovalStage { Pending, Approved, Rejected }

entity Invoice {
  [Required] decimal Amount;
  ApprovalStage Stage;
  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

workflow ExpenseApproval {
  Tracks    = Invoice.Stage;
  Autostart = true;
  Initial   = Pending;

  event Decide(bool approved);

  Start {
    if (this.Item.Amount <= 500) { goto Approved; }   // a small one is approved on arrival; the rest start Pending
  }

  state Pending {
    subscribe Decide(bool approved);
    on Decide(bool approved) {
      when (approved) { goto Approved; }
      default { goto Rejected; }
    }
  }

  terminal success Approved { }
  terminal error   Rejected { Message = "rejected"; }
}
```

## Notes          {#notes}
**Every state must be a member of the tracked enum.** A state the enum does not name is a compile error — the two
declarations are one vocabulary, so they cannot drift apart either.

**`Autostart` decides when a run begins** — and therefore whether the field's opening value is the workflow's to
supply or yours to declare. That is the one thing to carry away from this page.

## See also       {#see-also}
- [subscribe](https://osysharp.com/reference/workflow/subscribe/) — declaring what a state waits for
- [complete when (a state's own completion condition)](https://osysharp.com/reference/workflow/complete-when/) — leaving a state on a condition rather than an event
- [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/) — starting child work from a state
