← Building agentic patient workflows

Part 5 of 18 · 8 min read

Patient journeys beyond the chat

Versioned journeys, transactional outbox events, worker leases and durable escalation.

Arunank Sharan · Kuzushi Labs ·

In this article

Patient relationship work rarely finishes in one exchange. Registration leads to an appointment. An appointment can create follow-up work. A reminder may fail, and someone has to own the next step.

We gave that longer process its own model: a journey definition and a patient-specific journey instance. The assistant can help operate it, while the journey remains visible outside the conversation.

Separate the process from a person's progress

A journey definition contains ordered stages. An instance connects that definition to a patient and can also reference an appointment or encounter. It records the current stage and overall status.

When an instance is created, the service verifies the referenced journey and patient, creates a status record for every stage and marks the first stage as in progress. Later stages begin as not started.

This makes progress inspectable. We can show where a patient is, which stages are complete and when a stage was entered. We do not need to infer the process from a sequence of messages.

Imagine a simple administrative follow-up: contact required, contact attempted, appointment arranged, follow-up closed. These are illustrative stages, not a prescribed care pathway. Their value is that the team can agree what each transition means.

Move the state, then record the consequence

Advancing a journey marks the current stage complete, stores any transition notes, and finds the next stage by its order. The next stage becomes active. If there is no next stage, the journey completes and clears its current-stage pointer.

The implementation publishes events for instance creation, stage entry and completion. Those events give other components a way to react without putting every downstream operation into the journey controller.

A stage can also schedule background work. The implemented stage-action example creates a pending communication record from configuration. That is a useful connection between process state and outreach, but a queued record is only the start of delivery.

Keep message creation and delivery separate

The codebase contains more than one generation of communication work. The basic communication service has a placeholder send routine that marks a record as sent without calling a provider. A separate omnichannel layer contains provider adapters, including WhatsApp and SMS integrations.

Those facts should remain separate. An adapter elsewhere in the repository does not prove that a journey-created message reached it. A stored “sent” value is not a delivery receipt.

To complete the journey-to-message path, I would trace one configured action through recipient resolution, provider submission and delivery status. The user interface should be able to distinguish pending, accepted by the provider, delivered and failed. A failure should leave an actionable task rather than an apparently finished stage with no visible explanation.

Events need a recovery story

The journey service commits state before publishing its event. This is an important implementation detail: if publication fails afterward, rolling back the session cannot undo the already committed transition.

The design therefore needs a recoverable record of work that remains to be published. An outbox is one way to provide that record. A consumer also needs to recognize repeated deliveries so that replaying an event does not produce duplicate outreach.

These are completion requirements for the event path, not capabilities I would infer merely from the presence of a message broker. I would test the transition with publication interrupted and then verify that recovery preserves one business outcome.

Make the handoff survive a process failure

The code's commit-then-publish pattern raises a precise failure question: what if the database commit succeeds and the process exits before publishing the event? A follow-up can exist without its downstream work being scheduled.

I would introduce an outbox row in the same transaction as the journey or appointment change. The application records the fact first; a worker publishes it afterward. This follows the transactional outbox pattern documented by AWS. It addresses the dual-write gap, but consumers still need duplicate handling.

BEGIN;
UPDATE journey_instances
SET current_stage_id = :next_stage,
    version = version + 1
WHERE tenant_id = :tenant
  AND id = :journey
  AND version = :expected_version;
-- Application must require exactly one updated row.
INSERT INTO outbox (id, tenant_id, kind, payload)
VALUES (:event_id, :tenant, 'journey.stage_entered', :payload);
COMMIT;

This SQL is a PostgreSQL-oriented reference sketch. The original journey service has its own schema; it does not already contain this migration. The version check prevents two coordinators from independently advancing the same stage. If it updates zero rows, the transaction must roll back rather than insert an event anyway.

A workflow transaction creates an outbox event, then a leased worker and deduplicating consumer handle delivery.

Lease the event and fence the acknowledgement

A worker can crash after claiming an event. A permanent processing=true flag would strand it. I would record a lease deadline and a unique lease token; another worker may reclaim it after expiry. Only the current lease holder may acknowledge completion.

event = lab.claim_event(now=120, lease_seconds=30)
if event:
    receipt = provider.send(event["tenant"], event["id"])
    lab.record_receipt(event["tenant"], event["id"], receipt)
    lab.acknowledge_event(
        event["id"], event["lease_token"], now=121
    )

In the reference lab, the fake provider supports deduplication by event ID. That property is intentional and does not imply that a real messaging API supports the same request semantics. A reclaimed worker can still encounter an external operation already performed by the previous worker. For a real connector, I would use its documented idempotency facility or enter reconciliation rather than assuming a lease guarantees one external send.

The acknowledgement token protects the local queue from a stale worker. It does not cancel an in-flight network request or enforce remote fencing. That distinction matters whenever someone describes a worker pool as “exactly once.”

Work through a missed follow-up

Imagine a clinic has an administrative follow-up stage due at 10:00. At 10:03, the stage action creates a reminder communication. The provider accepts it at 10:04, but there is no delivery callback. At 10:20, the system should show “accepted, delivery unconfirmed,” not advance the journey on the assumption that the patient read it.

I would configure a separate elapsed-time rule to create a staff task when delivery remains unresolved. Its deduplication key might be (journey_instance, stage_instance, rule_version). Re-running the timer would then find the same escalation task rather than create another one every minute.

The time limits are worked-example defaults. A clinic must choose them for its communication policy and operating hours. Administrative reminder logic should not infer clinical urgency from a language model's interpretation of a message.

Version the process as well as the record

A journey definition may change while patients are already enrolled. Adding a stage should not renumber every existing patient's progress. I would pin each instance to a definition version, assign stable stage identifiers and make migration an explicit operation with a preview of affected instances.

ChangeHandling I would implement
Correct a display labelCompatible metadata update
Add a new mandatory stageNew definition version and migration decision
Disable an outreach channelStop future dispatch; preserve previous receipts
Retry a failed stage actionNew attempt under the same logical action identity
Reopen a completed stageExplicit transition with actor and reason

The accompanying tests kill the operation between its local write and outbox insertion, reclaim an expired lease and reject an acknowledgement from the old worker. These checks are small, but they expose whether the workflow can continue after the happy-path request has ended.

Give people a place to pick up the work

Tickets and communication records complement the journey. A journey describes progress through a process; a ticket describes work someone needs to resolve. Conflating them makes exceptions difficult to manage.

The assistant's role is to help staff inspect that state and prepare the appropriate next operation. The durable records give the next person a way to continue when the assistant session is gone.

That is the capability I care about most here: the relationship workflow survives the conversation. The product can then improve its conversational interface without losing the operational structure underneath it.

The implementation kit includes the articles, diagrams and an offline reference lab. Code identified as a reference design is a new teaching implementation; it is not a claim that the original project already shipped that behavior.