← Building agentic patient workflows

Part 11 of 18 · 5 min read

A durable harness for patient work

Checkpointed workflows, approval resume, effect identities and recovery semantics.

Arunank Sharan · Kuzushi Labs ·

In this article

“Find the patient, book the follow-up and prepare the reminder” is one request to a person. It is several operations to the application, with a pause for approval in the middle. The harness has to remember which operation is next after that pause, a browser reload or a worker restart.

Our orchestrator already routes to specialist agents and supports a sequential chain. In the inspected path, a pending action stops that chain. Confirmation executes the saved agent plan, but does not automatically resume the remaining chain. The workflow-state module contains useful definitions, yet those definitions are not proof that every resume path is connected.

Persist the next step explicitly

I would give a workflow its own identity and state, independent of the chat session. A concrete booking-and-outreach workflow could look like this:

{
  "workflow_id": "wf-001",
  "definition": "book_and_prepare_outreach.v1",
  "revision": 6,
  "state": "awaiting_approval",
  "current_step": "confirm_booking",
  "completed_steps": {
    "resolve_patient": {"patient_id": "p-001", "version": 7},
    "select_slot": {"slot_id": "s-001", "version": 4}
  },
  "proposal": {"id": "proposal-001", "revision": 2},
  "next_on_success": "prepare_outreach"
}

This is a proposed application record. It should be stored under the tenant, with an owner, timestamps and a reference to the initiating actor. A Redis chat TTL can expire without deleting this workflow. Expiry of a proposal should move the workflow to a state requiring a new proposal rather than erase its history.

Persisted workflow checkpoints survive interpretation, human review, booking and downstream outreach.

Make transitions a function of events

The model can interpret a message within a step. It should not invent the workflow's state graph. I would begin with a transition table that a developer and operations lead can both review:

Current stateEventNext statePermitted effect
Resolving patientOne verified selectionSelecting slotAvailability query
Selecting slotSlot chosenAwaiting approvalSave proposal
Awaiting approvalMatching authorized approvalExecuting bookingCommit or invoke booking connector
Executing bookingVerified receiptPreparing outreachCreate communication proposal
Executing bookingUnknown outcomeReconcilingLookup by stable business ID
Any waiting stateAuthorized cancellationCancelledStop future work

The transition handler should use optimistic concurrency or a row lock. If two events both try to advance revision 6, only one should produce revision 7. The loser reloads state and decides whether its event is now duplicate, stale or still relevant.

def next_state(state, event):
    transitions = {
        ("awaiting_approval", "approval.accepted"): "executing_booking",
        ("executing_booking", "booking.succeeded"): "preparing_outreach",
        ("executing_booking", "booking.unknown"): "reconciling",
        ("reconciling", "booking.found"): "preparing_outreach",
    }
    try:
        return transitions[(state, event)]
    except KeyError:
        raise ValueError("Unsupported transition")

That function is deliberately incomplete: it illustrates the transition boundary, not a production engine. The implementation must persist the transition and any outbox work together. An exception should not leave the workflow advanced with no scheduled effect.

Give every effect a stable identity

I would identify a logical step by workflow ID, definition version and step ID. Retries add an attempt number but retain the logical operation identity. The external booking business ID should stay stable across attempts; generating a new ID on every retry defeats reconciliation.

The harness should record the adapter version, input fingerprint, start time, result category and receipt reference. It should avoid copying entire patient records into every checkpoint. A worker can load the authorized current record when needed, while the proposal retains the specific fields and versions that were reviewed.

An effect can be not_started, in_flight, succeeded, rejected or unknown. I would reserve unknown for a write whose external outcome cannot yet be established. Treating it as ordinary failure encourages unsafe retries.

Choose a framework for persistence semantics

LangGraph's interrupt mechanism provides a concrete human-review model: execution can pause and resume using persisted graph state. Its documentation also makes replay behavior relevant to where side effects are placed. I would evaluate it against the required checkpoint store, concurrency policy and connector semantics rather than assume that adding interrupt() makes writes idempotent.

An existing durable workflow engine may be preferable if the team already operates one and needs timers, retries and long-running histories. A small explicit state machine may be sufficient for a narrow administrative pilot. The deciding test is recovery: can a new worker continue from the saved state without repeating an already accepted booking?

Bound autonomous work

For the first implementation, I would set a maximum of six interpretation/tool iterations per interactive request, a separate connector timeout and an overall task deadline. A human approval wait should not consume a model execution budget. A timed-out model call may be retried under its own policy; a timed-out write may require reconciliation.

Background work also needs per-tenant limits. One clinic importing a large outreach campaign should not exhaust the workers needed for another clinic's interactive appointment confirmation. Separate queues or weighted scheduling can protect those classes of work.

Prove the resume path

The critical acceptance case starts with a paused workflow, then terminates the process. A new process receives the approval, verifies the proposal revision, completes the booking once and prepares the next communication. Replaying the approval must return the same booking result and must not create a second reminder.

I would also cancel while the workflow is waiting and while an external call is in flight. Waiting work can stop cleanly. In-flight work needs an explicit “cancellation requested; outcome pending” state until the connector reports what happened. That is the level at which a harness becomes an operational component rather than a loop around a language model.

The reference lab demonstrates the transactional approval and outbox pieces. This chapter's full workflow engine remains a proposed integration, with the resume acceptance cases supplied for implementation.