Journey execution

Table of Contents

1. Summary

A user journey is a design artefact, not a server-side object. A journey is executed by one of four composition classes, and the class decides where the transaction boundary sits: a direct journey makes one immediate call, a composite journey must land several writes together, a workflow journey runs work that takes time and must be resumable, and a read model journey never writes at all. A journey is not a saga: a saga is one consistency strategy a step may use, and it trades atomicity for recoverability. The entity subjects stay the only write path; a coordinator owns order, idempotency, retry and progress, and never field rules.

2. Detail

2.1. The vocabulary

Four words are in play and they are not synonyms. Each sits at a different level.

Term What it is Where it lives
user journey A design artefact: what one person is trying to do, written before anything is built. Its implemented form is a definition — an ordered list of steps. doc/knowledge/journeys/
step One entry in that list: an identity, a title, what it shows, what it does, and whether the person may return to it. The client half of the contract is Journey runtime. The journey definition, in the client
workflow instance Persisted progress: which journey, which step, what it is waiting for. ores.workflow
saga A consistency strategy for one step that must write several entities and must be recoverable rather than atomic. A step's implementation

A step is one of two kinds, and the kind decides whether it survives a reload.

  • A workflow step runs server work that takes time. Its progress is the workflow instance, so the person can leave and come back, and a retry resumes it.
  • A human step waits on the person: a sign-in, a new password, a party chosen. Nothing on the server records that the person is here, so the step does not survive a reload. The journey document must say which steps those are, because the screen has to be honest about it rather than implying the whole rail is resumable.

The temptation is to call the workflow instance a "journey" too, because it holds a journey's state. Resist it, because the journey is not server-side; its state is. That sentence settles the confusion, and it is why this page drops the phrase "server-side journey" altogether: it is a category error, not a distinction.

The same reasoning means no second entity is needed. workflow_instance already carries everything a journey's progress requires: type names the journey, beside state_id, step_count, request_json, result_json, error and completed_at. The chain the run was started with is not a column on it: the run's plan steps and the dependencies between them are their own relations, so a run is judged by the chain it was given rather than by the definition as it stands now. A journey is the definition; a workflow instance is that definition running. The bootstrap map's decision to add "a journey entity in ores.workflow, next to workflow instance" is therefore redundant, and the cheapest time to notice that is before it is built.

2.2. A saga is not a journey

A saga is a way to keep several writes consistent without holding one transaction: a sequence of local transactions, each with a compensating action, coordinated so the whole either completes or is undone in part. Its defining property is that it is not atomic, and that intermediate states are visible.

That makes it a different kind of thing from a journey, at a different level:

  • A journey is what the person does. A saga is how the system stays consistent.
  • One journey step may be a saga; a journey may contain no saga at all.
  • A saga can exist with no journey anywhere near it, as when one service calls another.

So "saga" cannot replace "journey" — doing so would name the person's experience after an implementation pattern. The useful move is the opposite: where a journey step is a saga, the journey document must say so, because the screen then has to be honest about partial state rather than implying the work either happened or did not.

2.2.1. History

Before 2026-09-23 the IAM knowledge doc defined a capital-J Journey as "a guided path that takes a person through workflows and their own actions in order", with the state kept on the server. The bootstrap map's decisions and prototypes use that word throughout. Those documents are the historical record and are left as they stand; the vocabulary above supersedes them.

2.3. The composition classes

Every journey falls into one of four classes. The class is a property of the journey, and it decides how much machinery the journey gets. Most journeys need none.

Class What it needs Journeys
Direct One immediate subject call. No coordinator, no state. Set default party; assign or revoke a role; role and lookup edits
Composite Several writes that must land together. Bring someone in (five writes); Change someone's details (two)
Workflow Work that takes time, can fail half way, must resume and report progress. Setup: First run, New tenant, New party
Read model Composition over reads, never writes. See who has access

Only two of the four want a service, and for different reasons: composite wants a transaction boundary, workflow wants resumable state. A read-model journey wants a query that returns the joined rows in one request, not a service and not a call per row.

2.4. Where the transaction boundary sits

This is the decision that the phrase "a service with the journey's interface" hides. A service that makes five subject calls still makes five transactions. The half-way failure does not disappear; it moves to the server, where it is harder to show the person what happened. Correctness across several writes needs one of two things, and the choice is per step:

  • One transaction. One subject, the server applying several tables' writes together. Atomic. Use it when the writes are one conceptual act that cannot sensibly be half-done — creating an account is the account, its contact record, its parties and its role, and the person thinks of that as one thing.
  • A saga. Idempotent steps, recorded progress, a compensating action each. Not atomic, intermediate states visible. Use it when the work is long or crosses services, and recovery matters more than isolation.

Choosing "a service" without choosing between these changes nothing observable, which is why the class must be recorded per journey rather than assumed.

2.5. Guard rails

A coordinator is a state machine over commands. It may own order, idempotency, retry, compensation and progress. It must not own field validation, business rules or SQL, and the entity subjects remain the only write path.

That is not a stylistic preference. The repo already carries the counter-example. account_setup_service is a coordinator, and its own header says:

Use this service instead of calling account_operations_service::create_account() directly to ensure accounts are properly initialized with roles.

A comment telling callers which of two creation entry points to use is a contract that cannot be enforced. The consequences are already visible: save_account_request carries account_type, the coordinator does not pass it on, and the layer beneath hard-codes new_account.account_type = "user";, so a service account cannot be created at all. The same shape appears one layer up, where account_handler.hpp and account_operations_handler.hpp both handle the account entity and the permission guard was applied to one and not the other — one of the ores.iam handler permission defects the security hardening has since closed.

So the guard rails are: one entry point per concern; a declared contract for what a coordinator must preserve; and a test that a request field reaches the table. Where a journey is composite, prefer a single subject over a coordinator, because it removes the seam rather than policing it.

2.6. What exists, and what is missing

Piece State
ores.workflow, with workflow_instance and workflow_step Exists
Internal actor impersonation, for a service acting as a person Exists
account_setup_service, the first coordinator Exists. account_operations_service::create_account takes no account type and hard-codes user; a separate create_service_account takes one and refuses user outright. That second entry point is the enforcement the guard rails above ask for, and it is why the defect is now a dead field rather than an impossible service account: save_account_request still carries an account_type that reaches neither of them.
Workflow progress reads: workflow.v1.ops.get_workflow_steps and workflow.v1.instances.list Exists, subscribed, and reached from the browser through its own route and wire-client method. A journey's rail is a rendering of the read.
The workflow_instance_changed event Exists, and the progress contract does not rest on it. Its subject matches the convention the BFF subscribes on, ores.<component>.<entity>_changed, but its payload does not match what the browser decodes: the engine sends an entity_change_event carrying entity_ids, and the shared browser schema reads alpha2_codes, a field named for the country event it was written for. Nor is there one shape to align the schema to: every entity's own event names its identifiers its own way. So a journey follows the run through the read, and the event channel is owned by its capture.
Tenant provisioning through the engine Exists. The generic provision request creates the tenant and its administrator and opens a run whose six step kinds execute.
A retry or resume verb Exists: workflow.v1.ops.retry_workflow_instance resumes a run that stopped on its failed step, re-dispatching that step under its own identity and keeping every completed step. Recovery on service start stays for runs that were in progress.
A generic iam.v1.ops.provision_tenant that returns an instance id at once Exists
A seed profile: its model, its table and its read Exists
A composite account-creation subject Missing
A joined roster read Missing
A journey entity beside workflow_instance Not needed; see the vocabulary above

Fifteen of the twenty-one journeys are runs of immediate operations, and one, See who has access, is a read model, so sixteen need none of the machinery above. The five that do are the three Setup journeys, which are workflows, and Bring someone in and Change someone's details, which are composite. A journey with several writes is still direct when each write is its own step and none has to land with another, which is the shape Register a service account has; it is direct for that reason, not because its step count is small.

3. See also

  • Data-Oriented Design in ORE Studio — a coordinator does not breach it. The regimes govern data layout, value semantics and projection, not where a transaction boundary sits. The one real cost is Regime 3's rule that the wire shape be a codegen projection of the entity: a journey's messages are hand-written, so they need their own Regime 3 walk.
  • User Journeys — the catalogue this classifies.
  • Identity and Access Management — where the old Journey definition lived.

Emacs 29.3 (Org mode 9.6.15)