Projo FHIR Documentation
0.1.0 - ci-build
Projo FHIR Documentation - Local Development build (v0.1.0) built by the FHIR (HL7® FHIR® Standard) Build Tools. See the Directory of published versions
Projo orchestrates work across four FHIR resource types whose state machines interlock: Task drives a clinical to-do; CommunicationRequest drives a transport action; Communication records the WhatsApp message itself; and QuestionnaireResponse captures the patient's answers. CarePlan holds the clinical envelope, and a webhook circuit breaker protects the inbound path from FHIR-server overload.
This page documents each lifecycle, the codes Projo writes, and the convergence points where one resource's state transition triggers the next.
A Task is the orchestrator's unit of clinical work — typically "deliver this
Questionnaire to this patient and collect the answers". Its FHIR status
follows the standard R4 progression; Projo additionally writes a
businessStatus (from the task-business-status code system) so that
operators and downstream subscriptions can distinguish why a Task is sitting
in a given status.
stateDiagram-v2
[*] --> requested : created by Task due event
requested --> ready : identity resolved
ready --> accepted : delivery channel picked up
accepted --> in_progress : first message sent / QR opened
in_progress --> completed : responses-collected + extraction ok
in_progress --> cancelled : cancelled by operator
accepted --> cancelled : cancelled by operator
ready --> cancelled : expired
completed --> [*]
cancelled --> [*]
The task-business-status code system carries the full set below. Projo
currently writes only the three marked active; the remainder are
historical and retained for forward-compatibility of the code system.
| Code | Projo writes | Meaning |
|---|---|---|
waiting-for-delivery |
active | Task created, identity resolved, awaiting transport pickup |
invited |
historical | Patient has been contacted at least once |
reminder-sent |
historical | A reminder has been dispatched |
collecting-responses |
historical | QR opened, answers being received |
responses-collected |
active | All expected answers captured, QR completed |
delivery-failed |
historical | Transport could not deliver after retries |
expired |
active | Task exceeded its due window without completion |
The active triple (waiting-for-delivery → responses-collected → expired)
is the only set the taskDue and communicationRequestStatus handlers emit
today. Any other code in this system is read-only signal for legacy data.
CommunicationRequest (CR) is the cross-app transport contract. Projo creates
four variants, each with its own terminal pair. All four start in active and
are driven to a terminal status by either the sidecar (outbound/reminder), the
orchestrator (reverse), or the web-functions runtime (web).
stateDiagram-v2
[*] --> active : CR created
active --> completed : outbound — sidecar sent
active --> revoked : outbound — sweeper timeout
active --> completed : reverse — resolved to single patient
active --> entered_in_error : reverse — multiple-matches
active --> completed : reminder — sidecar sent reminder template
active --> completed : web — browser POST received
active --> revoked : web — session sweeper TTL expired
completed --> [*]
revoked --> [*]
entered_in_error --> [*]
ready. The
sidecar's outbound subscription picks it up, sends the WhatsApp message,
and patches it to completed. A sweeper revokes CRs that exceed the
outbound delivery window without a send confirmation.completed; an ambiguous match
(multiple-matches) → entered-in-error.completed as soon as the reminder
template is dispatched.web-functions when a browser session opens a
QR channel. The browser POSTs answers; on final submission the CR is
completed. If the session TTL lapses before submission, a session
sweeper marks the CR revoked.When a patient sends STOP or UNSUBSCRIBE (any capitalization), the
sidecar files a reverse CR with reason stop-command instead of
advancing the conversation. The orchestrator's identity-resolution handler
then:
revoked).
Fire Arrow's CarePlanLifecycleInterceptor asynchronously deletes
non-terminal Tasks and disables Subscriptions.ready Tasks for the patient (status → cancelled,
businessStatus → expired). This removes them from
startNextReadyTask's search results so the CR revocation cascade
in step 3 finds nothing to dispatch.revoked).
Each revocation triggers communicationRequestStatus, which calls
startNextReadyTask — but all ready Tasks are already cancelled,
so it is a no-op.contentString
payload ("All protocols have been stopped and no further messages
will be sent."). The sidecar dispatches this as a freeform WhatsApp
text bubble inside the 24h customer-care window (opened by the
inbound STOP command).The revocation order (CarePlans → ready Tasks → CRs) is critical: if
CRs are revoked before ready Tasks are cancelled, the
startNextReadyTask cascade would find still-ready Tasks and
immediately dispatch them — re-engaging the patient with a new
questionnaire, the exact opposite of the STOP intent.
Confirmation messages are available in all supported languages:
en, de, es, fr, it, pt, nl.
A QuestionnaireResponse (QR) is the patient's captured answer set. Its lifecycle is short and intentionally simple — Projo does not author a questionnaire-specific state machine here, only the standard R4 progression.
stateDiagram-v2
[*] --> in_progress : first answer received / pre-populated
in_progress --> in_progress : per-answer update (If-Match)
in_progress --> completed : last answer submitted
completed --> [*]
input parameters before any patient
interaction.If-Match optimistic concurrency. The
orchestrator writes the version-aware update and relies on the FHIR
server's 412 Precondition Failed to reject concurrent writers.$extract and downstream clinical routing.Communication is the durable record of a single WhatsApp message. Outbound
messages start life as a placeholder (no wamid) and converge once the
WhatsApp API returns the platform message id; inbound messages are receipts
and are completed immediately.
stateDiagram-v2
[*] --> in_progress : outbound — placeholder created
in_progress --> completed : outbound — wamid returned by WA API
[*] --> completed : inbound — webhook = receipt
[*] --> entered_in_error : abandoned — with statusReason
in_progress --> entered_in_error : abandoned — with statusReason
completed --> [*]
entered_in_error --> [*]
in-progress with a placeholder body. On the
WhatsApp Cloud API response, Projo patches in the wamid extension and
flips status to completed.completed.entered-in-error with a statusReason explaining the failure mode.CarePlan is the clinical envelope for a patient's engagement: an intake
plan represents "this patient is onboarding", a follow-up plan represents
"this patient is now in programme X". Projo manages CarePlans entirely
server-side via $apply — clients never write them directly.
stateDiagram-v2
[*] --> active : $apply enrolls
active --> completed : supersede-then-enroll
active --> cancelled : operator / programme exit
active --> entered_in_error : corrective
active --> revoked : withdrawn
completed --> [*]
cancelled --> [*]
entered_in_error --> [*]
revoked --> [*]
scheduled (via the careplan-category tag) after
$subscribe-due-events enrolls the plan into the due-events schedule.clinicalRouting.ts:
the active intake CarePlan is moved to completed before the follow-up
plan is enrolled, so a patient is never simultaneously on two programme
plans for the same trigger.Identity resolution runs on every inbound message whose sender is not already attributed to a single patient. The reason code returned by the resolver drives a deterministic Projo response — there is no operator branch in the inbound path.
stateDiagram-v2
[*] --> resolving : inbound message
resolving --> no_match : zero candidates
resolving --> multiple_matches : >1 candidates
resolving --> non_conversational_only : only non-conversational matches
resolving --> no_active_cr : patient known, no open CR
resolving --> unresolved_receiver : receiver unresolvable
no_match --> [*] : create identity + apply triggered plan (onboarding)
multiple_matches --> [*] : mark reverse CR entered-in-error
non_conversational_only --> [*] : walk Person.link, else create
no_active_cr --> [*] : reattach + apply triggered plan (re-engagement/default)
unresolved_receiver --> [*] : upsert stub Organization + create identity + apply triggered plan
| Reason | Projo response |
|---|---|
no-match |
Create identity + apply triggered plan (onboarding) |
multiple-matches |
Mark reverse CR entered-in-error |
non-conversational-only |
Walk Person.link, else create |
no-active-cr |
Reattach + apply triggered plan (re-engagement/default) |
unresolved-receiver |
Upsert stub Organization + create identity + apply triggered plan |
The reason code is persisted on the resulting Communication (extension) so that downstream subscriptions and operators can replay the decision path.
Extraction and routing are the two convergence points where multiple lifecycles collide: a completed QR triggers extraction, and a successful extraction triggers clinical routing. Both are designed to be idempotent and self-healing.
stateDiagram-v2
[*] --> qr_completed : QR.status = completed
qr_completed --> extracting : SDC $extract invoked
extracting --> committed : atomic transaction succeeds
extracting --> failed : transaction rolls back
failed --> extracting : self-healing sweep (oldest-first)
committed --> [*]
completed is the trigger for SDC $extract.Observation / Condition / Provenance resources, keyed by
deterministic If-None-Exist keys so re-runs are no-ops.stateDiagram-v2
[*] --> extraction_ok : atomic transaction committed
extraction_ok --> routing : routeClinicalPlanIfNeeded
routing --> cql_gated : $apply (no _persist)
cql_gated --> bounded_action : single action selected
bounded_action --> enroll_plan : supersede intake, then enroll follow-up
bounded_action --> supersede_plan : complete active intake CarePlan
bounded_action --> route_to_review : raise operator Flag
bounded_action --> anchored : routing Provenance anchors idempotency
anchored --> [*]
routeClinicalPlanIfNeeded is invoked.$apply without _persist — the
PlanDefinition proposes, the orchestrator decides.enroll-plan, supersede-plan, or route-to-review. Ambiguity (multiple
enroll-plan proposals) routes to review rather than picking arbitrarily.The whatsappWebhook function has two execution paths: an inline path
that processes the webhook synchronously within the HTTP request, and a
queued path that enqueues and returns 200 immediately. ADR 0005
introduces an in-process circuit breaker that sheds the inline path when
the FHIR server is overloaded, so Meta's webhook retries are not burned on
FHIR latency.
stateDiagram-v2
[*] --> closed : steady state
closed --> open : inline > WEBHOOK_INLINE_TIMEOUT_MS
open --> half_open : WEBHOOK_CB_COOLDOWN_MS elapsed
half_open --> closed : probe succeeds
half_open --> open : probe fails
closed --> [*]
| State | Behaviour |
|---|---|
| Closed | Inline path runs. Webhooks are processed synchronously. |
| Open | Inline path is skipped. Webhooks are enqueued only; the function returns 200 immediately. |
| Half-open | A single probe attempt runs inline. Success re-closes; failure re-opens. |
WEBHOOK_INLINE_TIMEOUT_MS (default 8000 ms).WEBHOOK_CB_COOLDOWN_MS (default 60 s) elapses,
the breaker moves to half-open and admits one probe.The breaker is in-process (per function instance), not distributed. Each instance independently sheds load; recovery is per-instance. See ADRs for the full rationale.