Build and run a vacation approval
You finish with a completed vacation request, its decision and a record of who answered. Afterwards, try changing the approver or restarting while a request waits.
You need the starter set up. Run commands from its folder. Start IdentityFlow below, then sign in as the requester. Approval waits until an eligible account answers.
npm run workshop:startThe command starts PostgreSQL, the local sign-in service and IdentityFlow, and
keeps running in this terminal with the program’s output. It is ready when it
prints workshop: ok program ready at http://127.0.0.1:4173 and then
workshop: sign in at http://127.0.0.1:4173/auth/login. Leave it running and
use a second terminal for the other commands on this page. If the starter is
still running in the background from the setup, the command prints its status
and does not start a second copy.
- Open
http://127.0.0.1:4173/auth/loginand choose Continue as requester. - Open Workflow catalog and choose Start next to Vacation approval.
- Under Parameters, choose Requesting employee (requester) as
Employee, enter a First day and a Last day such as
2026-10-05and2026-10-09, and a Reason. Choose Start Workflow.
IdentityFlow opens the request under My work with the input you submitted.
The instance has run load vacation request and now waits at
decide vacation request. The requester cannot answer it: acme does not
report role:vacation-approver for this account.
- Choose Sign out in the account menu. The sign-in page opens again.
Choose Continue as vacation-manager. For this account,
acmereportsrole:vacation-approver; seefixtures/principals/accounts.ts. - Open Assigned to me. The entry shows the step name and the dialog’s
message, such as
Vacation request from …, 2026-10-05 to 2026-10-09 (5 days). - Open it. What you are deciding shows the request. Under Your answer,
set
decisiontoapproved, write anote, and choose Submit answer.
The page confirms Your answer was recorded. and the instance completes.
Answer validation and accounts without an assignment
The answer must match the dialog’s schema: decision is approved or
rejected, and note is text. If the page shows a text box Answer as JSON
instead of fields, enter { "decision": "approved", "note": "Enjoy the break" }.
An answer in the wrong shape is refused: the page lists what to correct, and
the dialog keeps waiting.
If you sign in as vacation-deputy or unrelated while the request waits,
Assigned to me stays empty: neither account holds
role:vacation-approver.
Stay signed in as vacation-manager. authorization.ts gives the holder of
role:vacation-approver the access functions
viewer, operator and auditor for this workflow, so this account sees all
three views of the instance.
Open Instances and choose the request. The address is
/instances/<id>; <id> is the instance ID. The instance has three tabs:
- Business shows
vacation-approval@v1.0.0 · latestand the status of the instance. Business data holds the result: the request as the directory step loaded it, and your decision. Business steps listsload vacation requestanddecide vacation request, each with its status, its start and finish time, and its step ID. - Audit (
/instances/<id>/events) opens Audit evidence: the number of events and one entry per event, in order. The entry for your answer has the typeSTEP_CONTINUING, the ID of the account that answered, and the authority it was accepted under: the principalacme/role:vacation-approver. Audit trail explains every field. - Operations (
/analytics/<id>) is where an operator pauses, resumes or terminates an instance.
The events are the source. The instance’s state and its steps are derived from them and can be rebuilt; the events themselves are never edited.
The requester has starter and viewer but not operator or auditor, so it
sees the business view without the other two tabs.
How the workflow and its files produce this result
The workflow is src/workflows/vacation-approval.workflow.ts. Its default
export is the Workflow Definition
vacation-approval, version 1.0.0. IdentityFlow deploys only a workflow
file’s default export; the file exports the same definition by name as well,
for the tests. The definition has these parts:
- Input schema.
VacationRequestat the top of the file lists the four parameters:employeeId,from,toandreason. IdentityFlow validates every start against it and builds the start form from it. - Step
load vacation request. Aflow.dostep that reads the employee through theStaffDirectorybinding, the workflow’s named access to the staff directory, and counts the requested days. - Dialog
decide vacation request. A dialog is a step that waits for a person’s answer. This one is assigned to the principal{ providerId: 'acme', subject: 'role:vacation-approver' }: a name that the provideracme, the starter’s identity hook, reports for every account that holds the role. Whoever holds it may decide. The dialog also carries an assignee source, which the section Change the approver uses.
Each step has a fixed name, and always the same name. Those names are how a resumed instance matches itself back to what it already did. Rename a step while instances are running and the engine runs it again. For the same reason, keep the body of each step safe to run twice: a retry can reach the system on the other end more than once.
Workflow state, meaning what ran, what it returned, and where the instance is now, belongs to IdentityFlow and lives in PostgreSQL. Everything else, such as the staff directory, is reached through a binding. A direct call to an external system from workflow code bypasses the fixture that replaces the binding in tests.
The other files involved:
| File | Contents |
|---|---|
src/bindings/staff-directory.binding.ts | The StaffDirectory binding. |
fixtures/bindings/staff-directory.ts | The prepared directory entries. |
fixtures/principals/accounts.ts | The seven prepared accounts and the principals acme reports for each. |
fixtures/assignees/vacation-approval.ts | The assignee source and its two states, standard and coverage. |
src/hooks/acme.hooks.ts | The provider acme, the resolver for the assignee source, and the webhook for change events. |
authorization.ts | Who may start the workflow, see its instances, operate them and read their audit record. |
tests/workflows/vacation-approval.workflow.case.ts | The Workflow Tests for this workflow. |
Two recipes show the same pieces as single files with their tests: Build a start form from the input schema and Assign a dialog to a role.
npm run test:workflowThe command runs the tests that vitest.config.ts selects,
tests/**/*.case.ts and tests/**/*.test.ts, with the Workflow Test
distribution in bin/. Each run starts its own PostgreSQL container and
removes it at the end, so Docker has to run. It does not need
npm run workshop:start and does not touch the starter’s database.
The tests for this workflow are in
tests/workflows/vacation-approval.workflow.case.ts:
- approves a vacation request and denies every other Account registers a
fixture, a test replacement, for
the
StaffDirectorybinding before it starts the workflow asrequester. It waits for the dialogdecide vacation request, checks thatrequester,vacation-deputyandunrelatedare refused withWF_TEST_MUTATION_REJECTED, answers asvacation-manager, and checks the result, the two completed steps, and the principal recorded on theSTEP_CONTINUINGevent. - records a rejection as a completed business outcome answers
rejectedand checks that the instance still completes.
The tests use Vitest’s test and expect, but they run only through
npm run test:workflow. Running vitest directly on a *.case.ts file fails
on purpose, because the Workflow Test runtime is part of the distribution, not
of the npm package.
Test workflows shows the same pattern, a binding, a workflow with one dialog and its test, as a project you set up without the starter. Test who may act explains the refused answers.
The line manager goes on vacation, and the deputy decides in the meantime.
The dialog carries an
assignee source next to its
assignees: a reference to a group that another system maintains, which
IdentityFlow resolves again when that system sends a change event. The
starter’s source, vacation-approval-team in
fixtures/assignees/vacation-approval.ts, has two states. In the standard
state the eligible principal is role:vacation-approver, which
vacation-manager holds. In the coverage state it is role:vacation-coverage,
which vacation-deputy holds.
assigneeSource: vacationApprovalSource(),- resolved on change event
- provider, key, version
-
As
requester, start another vacation request and leave it waiting. -
In the second terminal, switch the prepared data to the coverage state:
Terminal window npm run workshop:refreshThe command changes the prepared data and sends one change event to the webhook in
src/hooks/acme.hooks.ts, the way a real directory would. It waits until IdentityFlow has processed the event and printsapplied vacation-approval-team:coverage:v2; eligible now: role:vacation-coverage. If no dialog was waiting, it says so; start a request and run it again. -
Sign in as
vacation-manager. The request is no longer under Assigned to me. -
Sign in as
vacation-deputy. The request is under Assigned to me. Answer it.
Two mechanisms are at work, as
Assignee sources
describes. When the change event arrives, IdentityFlow calls the resolver in
acme.hooks.ts for every waiting dialog that uses the source and records the
new assignees as a STEP_ASSIGNEES_UPDATED event. The dialog keeps its step
ID, its name and its history, and the events recorded before the change stay
as they were. When someone answers,
IdentityFlow checks their authority again:
it asks acme for the account’s principals at that moment and compares them
with the dialog’s current assignees.
This is not delegation: nobody acts on someone else’s behalf. The deputy answers under a principal of their own. IdentityFlow 0.3.1 has no delegation; see Release 0.3.1.
A Workflow Test cannot send a change event. The test file
tests/workflows/vacation-approval.coverage.case.ts shows the other way access
can change: the deputy gains role:vacation-approver itself, and no event is
needed, because IdentityFlow reads the account’s principals at the moment it
answers.
Switch the prepared data back before you continue:
npm run workshop:refresh -- --scenario standardIf no request is waiting, the command says so; the prepared data is switched
back anyway. Every npm run workshop:start also resets the prepared data to
the standard state.
- As
requester, start a request and leave it waiting. - Stop IdentityFlow with Ctrl-C in the terminal that runs
npm run workshop:start. If you started it with--background, runnpm run workshop:stopinstead. Both stop the program and the containers and keep the database. - Start it again with
npm run workshop:start, and sign in asvacation-manager.
The request is still under Assigned to me, at the same step. The instance picks up where it was, because the dialog and its step identity are rebuilt from the recorded events. The instance does not depend on the stopped process.
Do not use npm run workshop:stop -- --volumes here; it deletes the database.
Events are never edited or deleted. If an instance went wrong, fix the code, raise the workflow’s version, and start a new instance; the old one stays as the record of what happened.
The workflow on this page is finished. To write its parts yourself, work
through these practice tasks in exercises/:
02-input-schema: the input schema and the start form;03-step: aflow.dostep;05-dialog: a dialog that waits for whoever holds a principal.
Each practice task’s folder has a README.md with the task, a start/ folder where
you work, and a solution/ folder. Check your version, or run the solution:
npm run exercise -- 05npm run exercise -- 05 --solutionA practice task first type-checks, then runs its tests. These three tasks use
their own test selection and do not change src/workflows/, and they need
Docker but not npm run workshop:start.
Next: Change a multi-step approval.