Skip to content

Day 1: Vacation approval

Day 1 builds one workflow: someone requests vacation, someone else approves it. It’s small on purpose — the point is not the process, it’s watching a running function survive a wait, a restart, and a change of approver.

Work in the starter you were given. The starter holds the code you run; this page tells you what to look at, what to expect, and what to keep.

Before changing anything, find four things in the starter: the workflow definition, its input schema, the step where it waits for an approval, and the prepared identity data.

There is one boundary worth internalising early. Workflow state — what ran, what it returned, where it is now — belongs to IdentityFlow and lives in PostgreSQL. Everything else, like the staff directory, is reached through a binding that the workflow asks for by name. If you find yourself calling an external system directly from workflow code, you’ve crossed that line.

Gate G3 — package publication. You need the 0.2.0 packages, either from a registry or as the tarballs you were handed. Until they’re there, the install step won’t work.

Gate G4 — required implementation. The exercise is executable when the starter and its pinned dependencies pass their own checks. Until then, read along instead of running along.

A workflow is defineWorkflow(options, execute): options carries the name, version, and input schema, and execute is an async function that does the work. Give every step a name, and always the same name — those names are how a resumed run matches itself back to what it already did. Rename a step mid-flight and the engine will run it again.

The vacation request validates its input, loads the person through a binding, waits at a dialog for the approver, and returns the decision.

Keep the body of each step safe to run twice. A retry can reach the system on the other end more than once, and that system has to be fine with it.

For the exact code — binding, workflow, and test — use Testing Workflows. Those blocks are compared byte-for-byte with a project that actually compiles, so they can’t drift. Don’t copy a sample from an older page.

Start the request as the requester. You should see it stop at a pending approval. Complete it as the prepared approver and collect the result.

Write down the instance ID and the fact that the step went from pending to completed. You’ll compare against this later.

What you should see: a completed run whose result contains both the person that was looked up and the approved decision. Anything else is a broken setup, not a newly discovered feature — go to Troubleshooting.

The test is a normal Vitest test. workflowTest() gives you a test function with three extras: a driver that can act as different people, the accounts you declared, and a registry for replacing bindings.

The shape is always the same: register the test double for the binding before start(), start the run as one person, wait for the dialog to appear, try to complete it as someone who shouldn’t be allowed, then complete it as someone who should. Assert both the returned result and the events you observed.

This runs against real PostgreSQL. An in-memory stand-in is not the same thing and does not count — half of what you’re testing is how state is written and read back.

The exact test, and the error you get when you forget the test double, are in Testing Workflows.

Three different things, easy to confuse:

  • Instance state — where the run is right now.
  • Steps — what ran, and how each one ended.
  • Events — the immutable, chronological record of everything that happened.

The events are the source. State is derived from them and can be rebuilt; the events themselves are never edited. When state and events disagree, the events are right.

Look at all three for your completed run. Keep payloads and credentials out of any screenshot you take.

The approver goes on holiday. Change which person the prepared source says is eligible, and submit the signed change event.

Do this against the running application, not from inside a workflow test: the signed source-change event arrives through the webhook, and the public test contract has no way to deliver one.

Watch what happens to the approval that is already pending: it is re-checked against who is a member now. It is not quietly rewritten, and the events recorded before the change are untouched. Verify both sides — the new approver can now see and complete it, and the old approver is turned away.

This works because the identity data is prepared and deterministic. It shows one specific thing: that membership is re-read at the moment someone tries to act. It is not a general “act on behalf of someone else” feature — that doesn’t exist. See Preview.

Stop the process while a run is sitting at a pending approval. Start it again.

The run picks up where it was, because the dialog and its step identity are rebuilt from the events. Nothing was held in memory.

If a run is wrong, never fix it by editing an event or deleting the stream. Fix the code and start a new run; the broken one stays as the record of what actually happened.

You’re done with Day 1 when you have all of this saved:

  • the starter revision you worked from
  • one completed run against the prepared data
  • the passing test output
  • the list of events that run produced
  • the before and after of the coverage change

Then continue with Day 2: Multi-role approval.