Skip to content

Build and run a vacation approval

Request vacation as requester Approver decides vacation-manager Read the history as the approver waits then
Request vacation as requester Approver decides vacation-manager Read the history as the approver waits then
The requester starts; the approver decides and reads the history.

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.

Terminal window
npm run workshop:start

The 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.

  1. Open http://127.0.0.1:4173/auth/login and choose Continue as requester.
  2. Open Workflow catalog and choose Start next to Vacation approval.
  3. Under Parameters, choose Requesting employee (requester) as Employee, enter a First day and a Last day such as 2026-10-05 and 2026-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.

  1. Choose Sign out in the account menu. The sign-in page opens again. Choose Continue as vacation-manager. For this account, acme reports role:vacation-approver; see fixtures/principals/accounts.ts.
  2. 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).
  3. Open it. What you are deciding shows the request. Under Your answer, set decision to approved, write a note, 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 · latest and the status of the instance. Business data holds the result: the request as the directory step loaded it, and your decision. Business steps lists load vacation request and decide 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 type STEP_CONTINUING, the ID of the account that answered, and the authority it was accepted under: the principal acme/role:vacation-approver. Audit trail explains every field.
  • Operations (/analytics/<id>) is where an operator pauses, resumes or terminates an instance.
TABS OF AN INSTANCE Event history the record Instance and steps derived Business viewer Operations operator Audit auditor derives business data diagnostics lists events
TABS OF AN INSTANCE Event history the record Instance and steps derived Business viewer Operations operator Audit auditor derives business data diagnostics lists events
Each tab of an instance needs its own access function; Audit lists the events, which are the record, and Business and Operations show the state derived from them.

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. VacationRequest at the top of the file lists the four parameters: employeeId, from, to and reason. IdentityFlow validates every start against it and builds the start form from it.
  • Step load vacation request. A flow.do step that reads the employee through the StaffDirectory binding, 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 provider acme, 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:

FileContents
src/bindings/staff-directory.binding.tsThe StaffDirectory binding.
fixtures/bindings/staff-directory.tsThe prepared directory entries.
fixtures/principals/accounts.tsThe seven prepared accounts and the principals acme reports for each.
fixtures/assignees/vacation-approval.tsThe assignee source and its two states, standard and coverage.
src/hooks/acme.hooks.tsThe provider acme, the resolver for the assignee source, and the webhook for change events.
authorization.tsWho may start the workflow, see its instances, operate them and read their audit record.
tests/workflows/vacation-approval.workflow.case.tsThe 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.

Terminal window
npm run test:workflow

The 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 StaffDirectory binding before it starts the workflow as requester. It waits for the dialog decide vacation request, checks that requester, vacation-deputy and unrelated are refused with WF_TEST_MUTATION_REJECTED, answers as vacation-manager, and checks the result, the two completed steps, and the principal recorded on the STEP_CONTINUING event.
  • records a rejection as a completed business outcome answers rejected and 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(),
  1. resolved on change event
  2. provider, key, version
  1. As requester, start another vacation request and leave it waiting.

  2. In the second terminal, switch the prepared data to the coverage state:

    Terminal window
    npm run workshop:refresh

    The 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 prints applied 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.

  3. Sign in as vacation-manager. The request is no longer under Assigned to me.

  4. 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.

workshop:refresh coverage state IdentityFlow webhook Resolver acme.hooks.ts decide vacation request now role:vacation-coverage vacation-manager role:vacation-approver vacation-deputy role:vacation-coverage change event calls replaces assignees refused answers
workshop:refresh coverage state IdentityFlow webhook Resolver acme.hooks.ts decide vacation request now role:vacation-coverage vacation-manager role:vacation-approver vacation-deputy role:vacation-coverage change event calls replaces assignees refused answers
The change event makes IdentityFlow resolve the assignee source again and replace the waiting dialog's assignees, so the deputy may answer and the manager is refused.

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:

Terminal window
npm run workshop:refresh -- --scenario standard

If 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.

  1. As requester, start a request and leave it waiting.
  2. Stop IdentityFlow with Ctrl-C in the terminal that runs npm run workshop:start. If you started it with --background, run npm run workshop:stop instead. Both stop the program and the containers and keep the database.
  3. Start it again with npm run workshop:start, and sign in as vacation-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: a flow.do step;
  • 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:

Terminal window
npm run exercise -- 05
npm run exercise -- 05 --solution

A 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.