One Apex service shared by Azure and the data-entry screen flow,
replacing today's 7,904-line transcription monolith. Phase 3 of the
Azure roadmap — the project that lets Azure write the full intake
graph (patient + order + prescriptions + notes), instead of staging an
image for a human to transcribe.
Active · Phase A authoring nextOwner: KyleLast verified 2026-05-06
4
phases of automation
P0 live · P1 in progress · P2 next · P3 = this plan
3 → 11
SF objects Azure writes
today: ContentVersion + Rx_Image + Case · target: full intake
graph
~150 → ~30
screen-flow nodes after refactor
load-bearing transcription flow → calls Apex once
5 / A→E
phases of build
A0 discovery complete · A authoring next
§1The big picture
Same pipeline, four states. Toggle the phase to watch what Azure
writes today (Phase 0), what's about to flip on (Phases 1 + 2), and
what this plan adds (Phase 3). Every other phase-aware section on the
page reacts to the same toggle.
Show pipeline at:
External systemAzureSalesforceSF record / objectHuman action
Dashed + dimmed = not active in this phase
Phase 0 · live today
What's lit up: LifeFile → PAD → Logic Apps writes
ContentVersion + Rx_Image__c +
Case (RT=Data_Input). Staging Case lands
with ContactId=null.
Everything past the staging Case is human
transcription.
The DE team carries every e-script.
Phase 1 · in progress
What's new: ADF syncs Contact roster
into Azure SQL; AI Search scores incoming patients;
high-confidence matches arrive with
Case.ContactId pre-populated. DE workload shrinks for
matched patients (review, don't lookup). Flag:
Escript.Evaluator.RequirePatientMatching false →
true.
SF-side perm-set expansion lands with the Azure flag flip —
neither alone is functional.
Phase 2 · next
What's new: same AI-match approach, applied to
prescribers. Case.EHR_Practitioner__c already exists
and wf-sf-writer already writes to it — currently
resolves to null because the flag is off. Phase 2 is a
feature-flag flip on the Azure side,
not a new SF field addition.
Phase 3 · this plan
What's new: Azure stops posting an image and
starts posting a domain JSON to a new Apex
@RestResource
(PatientOrderIntakeRest). The endpoint owns the full
intake graph creation: Account → HC-auto-Contact → EhrPatient →
ContactPointAddress → Order → Rx[] → Pharmacy_Notes → new-order
Case. The data-entry screen flow gets refactored to call the
same service via an @InvocableMethod.
One Apex layer, two surfaces. DE team becomes the
fallback path for low-confidence + parser-failed payloads.
§2Phases in depth
Each phase is additive — earlier phases stay live as later phases
land. The panel matching your current phase toggle gets the strong
border + shadow.
Phase 0
Live today
Active in production
Today's state. Azure scrapes LifeFile and creates the 3-record
staging set per e-script. The data-entry team transcribes everything
else by hand.
What's running
Daily 8 AM wf-start-lifefile-automation kicks the
PAD bot
Every e-script becomes a DE work item — no automation past
staging
12-flow Case collision fires on every Azure-created Case
DE workload scales linearly with intake volume
Phase 1
Patient matching
In progress
ADF syncs the SF Contact roster to Azure SQL; AI Search scores
incoming patients against the roster; high-confidence matches arrive
with Case.ContactId pre-populated. Pure flag flip on
the Azure side — paired with a SF perm-set expansion.
What's new
ADF outbound Contact sync to Azure SQL
AI Search scoring (phone, DOB, name + 1,100+ nicknames)
wf-sf-writer calls AI Search per e-script
High-confidence matches →
Case.ContactId pre-populated on insert
Low-confidence → unchanged (Case lands unlinked)
Feature flag flip
RequirePatientMatching false →
true
Confidence threshold tunable Azure-side without an SF deploy
What's de-risked
DE keeps human review (advisory match, not authoritative)
Threshold safety net via Azure-side tuning
Backward compatible: low-confidence stays on the existing path
Cutover gates & hazards
Interlock: SF perm-set expansion (Contact
Read + View All + FLS) MUST land WITH the flag flip
DE workflow change: some Cases pre-linked, some not
No SF-side audit trail today — confidence stays in Azure SQL
(recommend adding Match_Confidence_Score__c)
Phase 2
Prescriber matching
Next · pure flag flip
Same AI-match approach applied to prescribers. The
Case.EHR_Practitioner__c field already exists,
wf-sf-writer already writes to it, and it currently
resolves to null because the flag is off. Phase 2 is purely a flag
flip — not a SF schema change.
What's new
AI Search scoring extended to prescriber identifiers
SF write surface already in place (verified 2026-04-21 via
FieldDefinition)
No new SF field additions
No new perm-set expansion required for this phase
Cutover gates & hazards
Same DE-workflow-change concern as Phase 1
Same audit-trail gap (confidence stays in Azure SQL)
Phase 3
Structured intake writes — this plan
Authoring next (Phase A)
Azure stops POSTing an image and starts POSTing structured JSON to a
new Apex @RestResource. The endpoint creates the entire
intake graph in one transaction. The data-entry screen flow gets
refactored to call the same Apex service via an
@InvocableMethod wrapper — one source of truth for the
create-graph, two surfaces.
What ships
PatientOrderIntakeRest at
/services/apexrest/intake/v1/escript
C · Build REST endpoint — independent of B,
can run concurrent
D · Per-channel cutover — CMDT-gated,
e-scripts first, faxes later
E · Cleanup — only after D stable for one
release cycle
What's de-risked
A0 validated 8+ contract corrections against live schema and
screen flow
OrderItem VR-bypass requirement, auto-quarantine override,
3-FK integrity all documented
Idempotency design + structured error vocabulary defined
Sandbox test gate before A ships (HC auto-Contact-create under
with sharing)
Per-channel CMDT routing means rollback is a row flip — no
deploy
Risk register highlights
Sharing keyword decision — HC
auto-Contact-create may rely on system mode (Phase A test
gate)
OrderItem Prevent_Manual_Edits VR
— every update needs
Validation_Bypass_Date_Time__c
Auto-quarantine override on new-order Case
insert — silent RT/Owner rewrite
3 patient FKs on Rx must agree — no managed
integrity
Phase 3 perm-set blast radius — open decision
before Phase D
HC upgrade fragility — pin version in test
plan; rerun service tests after each upgrade
Logic Apps Scope_Catch_Failure
only triggers on HTTP error codes — 200-with-errors body
silently lost
§3Pipeline anatomy
Every moving part in the pipeline, in roughly upstream-to-downstream
order. Each card names what the component does, where its credentials
live, and how the integration breaks when the component does. Color
stripe matches the §1 diagram (slate=external · blue=Azure ·
indigo=Salesforce · amber=auth surface · dashed blue=Phase 3 future).
LifeFile
Upstream · external
The web app where prescribers' e-scripts arrive.
No API, no webhooks, no push. AdvancedRx's only
integration with LifeFile is browser automation against the UI as
a logged-in human user.
Auth
lifefile-username + lifefile-password,
loaded into the PAD bot from Azure Key Vault
Fails when
UI selectors change upstream → PAD scrape breaks (no API
contract to lean on)
Reference: azure-integration.md "Upstream source"
PAD bot
Power Automate Desktop
Daily 8 AM browser-automation bot. Logs into LifeFile, captures
each new e-script's image as PNG, and parses UI text into
structured JSON for downstream Logic Apps to consume.
Three workflows compose the pipeline:
wf-start-lifefile-automation (8 AM cron → triggers
PAD) → wf-sf-dispatch (per-document fan-out via
Service Bus) → wf-sf-writer (the Salesforce writer
with Scope_Catch_Failure). API version pinned in the
SF connection config to insulate against vendor drift.
One row per inbound e-script. Tracks DispatchStatus,
SalesforceStatus, AI confidence (PatientMatchScore
+ PatientMatchReasons +
MatchedPatientId), and any partially-written SF
record IDs. Stored proc
usp_UpdateSalesforceStatus uses
COALESCE so partial writes never overwrite real IDs
on retry.
Auth
Logic Apps managed connection
Fails when
Row stuck at Failed + Published —
wf-sf-dispatch won't auto-retry; manual replay only
Scores incoming patient identifiers (Phase 1) and prescriber
identifiers (Phase 2) against the Contact roster ADF synced into
Azure SQL. Signals: phone, DOB, name similarity (phonetic + 1,100+
nickname pairs). Threshold tunable Azure-side without an SF
deploy.
Auth
Internal Azure managed connection
Fails when
Roster stale (ADF lag); threshold tuned permissive →
false-positive matches reach PV1; threshold tuned strict → DE
workload doesn't drop
Reference: azure-integration.md "Outbound · Data
Factory Contact sync"
Azure Data Factory · ADF
Outbound Contact sync
Pulls SF Contact roster → Azure SQL on a scheduled cadence. Feeds
AI Search. Authenticates through the same
Azure Logic Apps Connected App as inbound — the
legacy Data Factory Connected App
(0H44W000000M5hgSAC) is a relic; SF no longer allows
creating that type, so both Azure workloads consolidate on one.
Auth
Linked service LS_Salesforce_Production →
Azure Logic Apps Connected App, OAuth Client
Credentials, scope api
Fails when
Connected App secret rotated without ADF refresh; perm set
narrowed below ADF's SELECT field list
Reference: azure-integration.md "Outbound" + "Why one
Connected App"
Connected App · Azure Logic Apps
SF auth surface
The SF-side OAuth surface for both Azure workloads.
Client Credentials flow. Scope = api ("Manage user
data via APIs").
Do not revert to full — Salesforce
deprecated full
for Client Credentials grants in Winter '26; reverting breaks both
pipelines silently.
Single Run-As user covers both inbound + outbound. Salesforce
Integration User License (no UI login). The
CreatedById
on every Azure-written record is this user — the strongest
writer-origin signal when debugging "where did this record come
from?"
Auth
Passwordless · OAuth-token-bound only
Fails when
Deactivation; license drop; profile reassignment; user-Id-based
VR bypasses authored against this Id
Reference: azure-integration.md "Auth chain"
Profile + Perm Set · least-privilege scope
Capability layer
Profile Minimum Access - Azure Integration is the
least-privilege base (CLAUDE.md §3.14 exemplar — the
one AdvancedRx-controlled integration that follows the convention
cleanly). Perm set Azure_Integration layers
capabilities.
Each phase expands what a compromised credential can
reach
— Phase 1 added Contact Read; Phase 3 will add full intake-graph
CRUD.
Maintenance
Layer new caps via the perm set; do
not bulk-edit the profile
Open question
Phase 3 perm-set blast radius: keep one perm set, or split a
Phase-3-only one? (Decide before Phase D.)
Reference: plan.md §6 risk #6 +
azure-integration.md "Why one user"
PatientOrderIntakeRest
Phase 3 · planned
The new Apex @RestResource Azure will POST to in
Phase 3. URL: /services/apexrest/intake/v1/escript.
Maps Azure JSON → IntakeRequest → calls
PatientOrderIntakeService.intake() → returns
IntakeResponse as HTTP body. Status codes drive Logic
Apps' Scope_Catch_Failure retry / DLQ logic (200-with-errors won't work
— see §9 hazards).
Twelve cards. Each names an SObject (or group) the pipeline touches,
who creates it, what fields are populated, and the per-record hazard
worth remembering. Phase 3 cards dim until you flip the toggle to P3.
Hover a dimmed card to read it without flipping.
Created by Azure today
P0+ · every phase
ContentVersion +
ContentDocumentLink ×2
standard SF
Created by:wf-sf-writer ·
Trigger: every inbound e-script
ContentVersion — VersionData (the PDF
/ PNG bytes), PathOnClient, Title
ContentDocumentLink #1 — links the file to
Rx_Image__c
ContentDocumentLink #2 — links the
same file to Case
Note: both links point at the same
ContentVersion — that's how SOQL "Cases linked to a
specific image" triangulates through
ContentDocumentLink.
Rx_Image__c
staging record
Created by:wf-sf-writer ·
Idempotent on: External ID (upsert)
External_Id_c__c — Rx External ID;
API name is typo'd (_c__c suffix).
Idempotent upsert key.
Image__c — HTML rich-text (32k limit) with embedded
<img> reference
Phase 3 finalize: service writes
Contact__c + EHR_Practitioner__c after
the intake graph lands
Don't autocorrect the
External_Id_c__c
API name — the typo is preserved deliberately. Renaming breaks
every reference.
Staging Case
RT=Data_Input
Created by:wf-sf-writer ·
Idempotent on:External_ID__c (clean
API name)
P1: + ContactId pre-populated when
patient match high
P2: + ContactId +
EHR_Practitioner__c both pre-populated when match
high
P3 finalize:ParentId → new-order
Case · Status='Closed' · Order__c set
· OwnerId untouched
12-flow Case collision fires on every insert.
Worst object-trigger collision in the org. Real per-update tax is
2 flows (the canonical Same/Related pair); the other 10 are gated.
Phase 3 — created by the Apex service
P3 · PatientOrderIntakeService
Account
RT=Individual
Created by:resolvePatient() ·
Skipped: existing-patient reuse path
RecordType.DeveloperName =
Individual (DeveloperName lookup, never hardcoded
ID)
Name =
"<lastName>, <firstName>"
HealthCloudGA__IndividualType__c =
'Group'
HealthCloudGA__EnrollmentType__c =
'NonDual'
SF_Manually_Entered__c = true
Inserting this triggers HC managed-package code
that creates a paired Contact automatically. The HC picklist
values are load-bearing for that auto-create — don't drift them.
Contact
RT=Patient · auto-created
Created by: Health Cloud package (server-side, on
Account insert) · Updated by: service post-create
Service writes after the auto-create:
FirstName, MiddleName,
LastName, Patient_Memo__c
Phone fields (Phone, MobilePhone,
HomePhone, OtherPhone,
SecondaryPhone__c) — formatted to exactly 10 digits
or rejected
5 active phone-format VRs require exactly 10
digits. Service uses a PhoneFormatter helper.
Don't insert Contact yourself — Health Cloud does
it.
HealthCloudGA__EhrPatient__c
third leg of patient triangle
Created by: service · Anchors:
Account + Contact + EhrPatient
HealthCloudGA__Account__c = Account.Id
Contact__c = the auto-created Contact.Id
(back-link)
Street, City, State,
Country, PostalCode — from payload
ParentId = Account.Id
(Master-Detail to Account, NOT Contact)
Contact__c = Contact.Id (custom Lookup)
Verification_Status__c /
Unverified_Reason__c — when address-validation
gating produced a result
Master-Detail is to Account, not Contact — common
assumption error. The Contact__c custom Lookup tracks
which Contact the address belongs to, but the parent
record is the Account.
Order
Status=Draft
Created by: service · Reused:
existing patient Draft Order if any
AccountId, BillToContactId,
ShipToContactId, Customer_Contact__c —
all → resolved Contact
SalesChannelId = lookup
WHERE SalesChannelName='AdvancedRx'
Intake_Idempotency_Key__c —
NEW field added in Phase A (Text(64), External
ID, Unique, Case-Insensitive)
Multi-Rx-same-day: if a same-day New-Order Case
already exists for the patient, attach to it instead of branching
a new Order. The service handles this lookup.
Drug data: Product__c, Sig__c,
Quantity__c, RefillsAuthorized__c,
HealthCloudGA__DateWritten__c
Date_filled__c — note typo'd lowercase 'd'
Price__c = 0.0 (initialized; updated by
applyPricing after Fills + OrderItems exist)
SF_Manually_Entered__c =
true (required) — triggers downstream
New_Prescriptions_Manually_Entered flow that
creates Fill + OrderItem
3-FK integrity hazard: the three patient-side
relationships MUST point at the same patient resolution result. No
managed integrity helps you here — drift is silently inconsistent.
New-order Case
RT may auto-quarantine
Created by: service · OwnerId:CS_Case queue (or admin if quarantined)
RecordType.DeveloperName =
Customer_Service_New_Order (typically) OR
Quarantined_Order (auto-override)
OwnerId = lookup
Group WHERE DeveloperName='CS_Case' AND Type='Queue'
Auto-quarantine override: if patient has any open
Case with RT=Quarantined_Order or
Refill_Request_Case, the canonical
Case_Insert_Update_Same_Record BeforeSave
silently rewrites RT and OwnerId. Gated on
ISNEW=true. Service post-queries the Case and returns
actualCaseRecordType + wasQuarantined in
the response.
Pharmacy_Note__c ×3
RT=Customer_Service
Created by: service · up to 3 per intake
'Active Portal Cart' on new-order
Case — when patient has an active WebCart with Rx CartItems
'Note from Data Input' on
new-order Case — free-form note from payload (or DE-team input
via refactored screen flow)
'Duplicate Order' on staging Case
— when intake detects this is a duplicate of an existing Order
All three use
RecordType.DeveloperName='Customer_Service' looked up
at runtime — never hardcoded. The
Pharmacy_Note__c object has only 2 RTs (Customer_Service
+ Pharmacist); these CS-bound notes correctly route
to the CS RT.
Phase 3 — created by downstream automation
P3 · fires from SF_Manually_Entered__c=true
Fill (HealthCloudGA__EhrMedicationPrescription__c
RT=Fill)
downstream flow
Created by:New_Prescriptions_Manually_Entered flow ·
Service does NOT create Fills directly
Fields cached from Master Rx: patient FKs · prescriber · product
· sig · quantity · strength · dosage form · days supply
NL_Prescription__c —
Fill → Master Rx parent FK
Patient_Fill_Id__c — populated by
HashUtils (MD5) for cross-system correlation
UnitPrice =
1.0 always — actual price lives in
TotalLineAmount + ListPrice
Prescription_Link__c = Fill record (custom FK)
Auto-created PricebookEntry uses Standard Price
Book — service should look up by
Name='Standard Price Book', NOT hardcoded ID
Validation_Bypass_Date_Time__c is mandatory.
Every OrderItem update by applyPricing() must set
Validation_Bypass_Date_Time__c = Datetime.now() or
the Prevent_Manual_Edits VR fails the update. The
single most consequential technical fact for Phase A authoring.
§5Auth chain
Both Azure workloads — inbound Logic Apps writes and outbound ADF
Contact reads — consolidate on one Connected App, one Run-As user, one
profile, one perm set. Single point of operational truth, single point
of compromise. The chain below is the inbound path; the outbound delta
is called out at the bottom.
Step 1 · Upstream01
LifeFile
(web UI)
No auth surface for AdvancedRx — passive UI we scrape, not an
integration partner.
Carries
The e-script document itself — viewed in a browser, no API
surface.
Creds
None on this surface. Auth happens at the next step (PAD logs
in as a human user).
Rotation
n/a — not a system AdvancedRx operates.
Compromise
If LifeFile creds leak: attacker can read prescriptions but
can't write to SF (different auth domain).
Step 2 · UI scrape02
PAD bot
Logs into LifeFile each morning as a human user; captures
images, parses JSON.
Carries
LifeFile session cookie for the duration of the scrape.
Creds
lifefile-username +
lifefile-password in Azure Key Vault, fetched at
run time.
Rotation
Rotate the LifeFile credentials in Key Vault; PAD picks up the
new values on the next 8 AM run.
Compromise
Equivalent to LifeFile creds compromise — read-only e-scripts,
no SF write capability.
Step 3 · Orchestration03
Azure Logic Apps
The orchestrator. Holds nothing sensitive locally — auth happens
at the next hop.
Carries
Parsed JSON + image bytes for each e-script.
Creds
Managed Identity on the Azure side (no stored creds for Azure
resources). Outbound to SF uses the Connected App.
Rotation
Managed Identity is auto-rotated by Azure. Connected App
secret rotation handled at step 4.
Compromise
Managed Identity scoped to the Logic App's resource group; not
directly exfiltrable. SF-bound auth gated by step 4.
Trust handoff · Azure → Salesforce
Step 4 · OAuth bridge04
Connected App · Azure Logic Apps
The single trust contract between Azure and Salesforce. Inbound
+ outbound share it.
Carries
Issues an OAuth access token bound to the Run-As user.
Flow
OAuth 2.0 Client Credentials. Scope = api (NOT
full — deprecated for CC in Winter '26).
Single rotation refreshes BOTH inbound + outbound auth —
coordinate during a maintenance window. Runbook:
.claude/runbooks/task-13-secret-rotation.md.
Compromise
Anyone with the consumer secret can mint tokens as the Run-As
user → step 5 capability set.
Step 5 · Run-As identity05
azure-integration@advancedrx.net
One user account fronts every Azure workload — the strongest
writer-origin signal.
Carries
The user identity tokens are issued for.
CreatedById on every Azure-written record.
License
Salesforce Integration User License (no UI login).
Rotation
n/a (passwordless). Deactivation = full integration outage;
treat as a load-bearing user.
Compromise
If the consumer secret leaks (step 4), an attacker can act as
this user — capability scoped at steps 6 + 7.
Step 6 · Profile06
Minimum Access - Azure Integration
The least-privilege base. Locked-down by default; capabilities
layered on top via the perm set.
Carries
Least-privilege base. Cloned from standard
Minimum Access; permissions added back
deliberately.
Reference
The CLAUDE.md §3.14 exemplar — the one
AdvancedRx-controlled integration that follows least-privilege
convention cleanly.
Maintenance
Do NOT bulk-edit the profile to grant new capabilities — layer
them onto the perm set instead (step 7).
Compromise
Profile-level perms are intentionally narrow; the real
capability surface lives in the perm set.
Step 7 · Permission set07
Azure_Integration
Where capability actually lives. Each phase expands the scope of
what gets reached.
Today
ContentVersion + Rx_Image__c + Case CRUD (inbound
writes).
Phase 1
+ Contact Read + View All (ADF outbound). Lands
together with the Azure-side
RequirePatientMatching flag flip — neither is
functional alone.
Phase 3
+ full intake-graph CRUD (Account, Contact, EhrPatient,
ContactPointAddress, Order, OrderItem, Rx, Pharmacy_Note__c,
Case + Rx_Image updates).
Open: keep one perm set or split a Phase-3 one?
Compromise
Each phase widens what a compromised credential reaches.
Document scope at each rollout.
Step 8 · API surface08
Salesforce REST API
What the integration user can actually do. Phase 3 adds the new
@RestResource; auth chain unchanged.
Today
Standard REST: insert ContentVersion +
Rx_Image__c + Case + ContentDocumentLink.
Idempotent upserts on External ID fields (External_Id_c__c
on Rx_Image, External_ID__c on Case).
Phase 3
+ POST /services/apexrest/intake/v1/escript →
PatientOrderIntakeRest. Same auth chain; new
endpoint.
Errors
4xx + 5xx surface to Logic Apps'
Scope_Catch_Failure → DLQ + SQL
Failed.
200-with-errors looks like success to
Scope_Catch_Failure — see §9 hazards.
Outbound delta (ADF Contact sync): identical chain
from step 4 down. The legacy Data Factory Connected App
(0H44W000000M5hgSAC) exists in the org but is not in use
— Salesforce no longer allows creating Connected Apps of that type, so
ADF authenticates through the same
Azure Logic Apps Connected App, runs as the same user,
uses the same perm set. The trade-off: rotating the consumer secret
touches both inbound and outbound at once.
§6Feature flags
Three flags govern the rollout — two on the Azure side, one new on the
Salesforce side that lands in Phase D. Phases 1 and 2 are
flag flips, not feature builds; the SF write surface for both
is already in place.
Flag
Lives in
Today (P0)
After flip
What flipping it does
Escript.Evaluator.RequirePatientMatching
Phase 1 trigger
Azure (wf-sf-writer evaluator config)
false — every Case lands
with ContactId=null, DE matches by hand
true — high-confidence
matches arrive with ContactId pre-populated
Azure AI Search starts scoring incoming patients against the
ADF-synced Contact roster. Threshold tunable Azure-side
without an SF deploy.
Escript.Evaluator.RequirePrescriberMatching
Phase 2 trigger
Azure (wf-sf-writer evaluator config)
false — outcome = "Ignored"
→ EHR_Practitioner__c resolves null even though
wf-sf-writer writes the field
true — high-confidence matches
set Case.EHR_Practitioner__c
Same AI-match approach, applied to prescribers.
Field already exists; wf-sf-writer already
writes it.
Phase 2 is a flag flip, not a SF field addition.
Intake_Pipeline_Routing__mdt
Phase D channel cutover
Salesforce (CMDT) — added in Phase D
n/a — doesn't exist yet
per-channel boolean — e.g.
eScript=true, fax=false
Per-channel + per-confidence-band routing. true =
Azure POSTs structured JSON to
PatientOrderIntakeRest; false =
falls back to today's image-staging path. Rollback = flip a
row, no deploy.
Phase 1 SF + Azure interlock: the
RequirePatientMatching flip on the Azure side AND the
Azure_Integration perm-set expansion (Contact Read + View
All + FLS on the synced fields) on the SF side must land
together. Either alone leaves the pipeline broken
intermediate — Azure asking for Contact data the integration user
can't read, or SF granting access nothing's reading. The
azure-integration@advancedrx.net user is already on the
perm set for inbound writes; Phase 1 widens its scope, no new perm-set
assignment needed.
§7The transcription handoff
14 flows reference Rx_Image__c in some way. The
load-bearing one is the 7,904-line screen flow the data-entry team
launches off the staging Case to produce structured records — Phase B
refactors that flow to call PatientOrderIntakeService via
an @InvocableMethod wrapper, preserving all the screens
but moving orchestration into Apex. Filter to scope the table.
The canonical transcription path. DE team
launches from the staging Case; produces Account →
HC-auto-Contact → EhrPatient → Order → Rx records →
Pharmacy_Notes → new-order Case. ~7,904 lines / ~150 nodes
today; Phase B refactors to ~30–40 nodes calling
PatientOrderIntakeInvocable.
New_Prescriptions_and_New_Order_Case
Screen
Active
Alternative hand-entry path (when there's no staging Case to
launch from). Phase B refactors this to call the same
invocable wrapper.
E_Script_Case_Creation
Record-triggered
Active
Creates a paired Case for a newly-staged Rx_Image. May
complement or supersede part of Azure's case-creation behavior
— verify before extending.
Image_Uploaded_to_Existing_Order
Record-triggered
Active
Manual-upload path — when an image is uploaded against an
existing Order (rather than via Azure ingest), this flow links
the image into the right Order context.
Rx_Image_Annotation_Button
Screen
Active
Pharmacist / DE-team rich-text annotation of the image.
Mutates Image__c; preserve the inline
<img> reference.
Pharmacist_PV1_Rx_Image_Annotation_Button
Screen
Active
PV1-time pharmacist annotation. Same surface as above;
pharmacist-action context.
Fax_Clarification_Flow
Screen
Active
Fax-path intake (distinct from e-script) — reads
Rx_Image__c for fax-clarification context.
New_Order_Flow
Screen / Autolaunched
Active
Order-side automation that reads Rx_Image__c for
context.
Order_Insert_Update_Same_Record
Record-triggered (Order)
Active
Order canonical BeforeSave — reads
Rx_Image__c for source-image lookup. Doesn't
mutate Rx_Image directly.
Order_Insert_Update_Related_Records
Record-triggered (Order)
Active
Order canonical AfterSave — also reads
Rx_Image__c for downstream linkage.
Automation_on_New_Refills
Record-triggered (Rx)
Active
Refill automation — reads Rx_Image for source-image audit
trail.
Rx canonical BeforeSave — reads Rx_Image for cross-record
validation.
FL_Loop_in_an_order_Final_Verification_view
Screen
Active
Final-verification screen flow — surfaces Rx_Image for
pharmacist review at PV checkpoint.
Data_New_Formula_or_Sig
Screen
Active
Misc data-entry helper that reads Rx_Image when the user is
creating a new Formula or Sig in context.
Update_Prescription_Image_via_Rx_Image
Record-triggered
Inactive
Predecessor of Image_Uploaded_to_Existing_Order.
Inactive but on disk — don't extend.
Update_Prescription_image_via_Rx_Image_preocess
Process Builder (typo'd preocess)
Inactive
Process-Builder sibling of the row above. Inactive. Migration
candidate; don't extend.
No flows match the current filter.
No canonical Same/Related pair exists on
Rx_Image__c.
Both record-triggered flows are functionally-named, not
Rx_Image_Insert_Update_*. New record-level automation
should consider introducing the canonical pair shape per
.claude/rules/flow.md.
§8One e-script's lifecycle
Walk one inbound e-script through the pipeline, step by step. The
stepper is phase-aware — switch the toggle (top right) to see how each
step changes across phases. Steps marked "P3" are introduced in Phase 3 and dim out in earlier phases.
Step 1 of 9
An e-script arrives at LifeFile
UpstreamLifeFile
A doctor sends a prescription to their pharmacy via LifeFile.
The document arrives in the doctor's LifeFile UI for the
pharmacy to retrieve.
AdvancedRx has no integration with LifeFile —
no API, no webhooks, no email or fax bridge. The only way to
discover documents is to log in as a human user and read them
off the screen.
Upstream UI dependency: if LifeFile changes
the e-script page layout, the scrape breaks. There's no API
contract to fall back on.
Step 2 of 9
The PAD bot scrapes LifeFile
Azurewf-start-lifefile-automationPAD bot
At 8 AM daily, Logic App
wf-start-lifefile-automation kicks off the Power
Automate Desktop bot.
The bot pulls lifefile-username +
lifefile-password from Azure Key Vault, logs in
as a human user, navigates the UI to the new e-scripts queue,
and for each document: captures the image as PNG, parses the
surrounding UI text into structured JSON.
The bot hands the parsed document set off to
wf-sf-dispatch for fan-out to the writer.
wf-sf-dispatch queues one Service Bus message per
document. wf-sf-writer consumes each and writes
the staging set to Salesforce in one logical sequence:
Insert ContentVersion — the file bytes (PDF or
PNG)
Insert Rx_Image__c — staging record with
rich-text Image__c + idempotent
External_Id_c__c
Insert ContentDocumentLink #1 — links the file
to the staging record
Insert Case with Subject =
"LastName, FirstName (DOB: MM/DD/YY)" ·
same-day repeats prefix "Multiple RX"
Insert ContentDocumentLink #2 — links the same
file to the Case (so it shows on the Case Files panel)
Idempotent on the External ID — if Azure replays the same
document later, both Rx_Image__c and
Case upsert instead of duplicating.
12-flow Case collision: the moment the Case
inserts, all 12 active Case-triggered flows evaluate. Real
per-update cost is the 2 canonical flows (Same / Related); the
other 10 are gated.
Step 4 of 9
Azure AI matches incoming patient (and prescriber)
Phase 0: this step doesn't run.
Escript.Evaluator.RequirePatientMatching = false.
Every Case lands with ContactId = null.
Phase 1+ (patient match):RequirePatientMatching = true.
wf-sf-writer calls Azure AI Search before the
Case insert. AI Search scores the incoming patient identifiers
against the ADF-synced Contact roster — signals: phone, DOB,
name similarity (phonetic + 1,100+ nickname pairs). If
confidence ≥ threshold, wf-sf-writer includes
ContactId in the Case insert from step 3.
Phase 2+ (prescriber match):RequirePrescriberMatching = true. Same approach
applied to prescribers. Case.EHR_Practitioner__c
pre-populated when match high. The field already exists and
wf-sf-writer already writes to it — Phase 2 is
purely a flag flip.
The match is advisory, not authoritative.
Pharmacy practice still requires DE / pharmacist review. Goal:
reduce lookup time, not remove human review.
Step 5 of 9
Logic Apps POSTs structured JSON to the Apex endpoint
Pre-Phase-3: this step doesn't run.
wf-sf-writer only writes the staging set; the
data-entry team produces structured records by hand from step
7 onward.
Phase 3: for high-confidence + parseable
payloads (gated by
Intake_Pipeline_Routing__mdt per channel),
wf-sf-writer POSTs to
/services/apexrest/intake/v1/escript. Body: the
parsed domain JSON — idempotency key, patient block,
prescriber block, prescriptions array, notes, address,
optional staging Case + Rx_Image IDs.
The PatientOrderIntakeRest endpoint deserializes
into IntakeRequest, calls
PatientOrderIntakeService.intake(), returns
IntakeResponse as the HTTP body.
HTTP status code drives Logic Apps' retry / DLQ
logic
— 200 = success (warnings allowed) · 400 = validation reject ·
409 = idempotency conflict · 500 = internal failure.
200-with-errors won't work. Logic Apps'
Scope_Catch_Failure triggers on HTTP error status
only. A pure-200 body with an errors[]
list would be silently treated as success — and every
validation failure would be lost.
Step 6 of 9
The Case lands in the work queue
SalesforceStaging CaseHumanDE team
Primary path (P0–P2): the Case appears in the
data-entry queue with subject "LastName, FirstName (DOB)". DE
picks it up, opens it, launches the screen flow. Every inbound
e-script flows through here.
Fallback path only (P3): only low-confidence
matches and parser-failures land here. The bulk of intake goes
through the structured POST in step 5 and never reaches the
queue.
Either way, the staging Case + image are the audit trail. Even
when the Apex service does the work, the staging Case is
finalized later (step 8) so the source-image lineage remains
intact.
Step 7 of 9
The structured graph appears
SalesforceAccountContact (HC auto)EhrPatientCPAOrderMaster Rx ×NFill + OrderItemPharmacy_Note ×3new-order Case
P0–P2 (screen flow path): the 7,904-line
New_Patient_New_Order_New_Rx_New_Case_Launch_from_data_case
walks DE through screens — patient demographics, prescriber,
up to 5 prescriptions, address, notes — then creates
everything in roughly the order shown above. ~150 nodes.
P3 (Apex service path): all of it runs in
one Apex transaction with a SAVEPOINT.
PatientOrderIntakeService.intake() does:
resolvePatient() →
resolvePrescriber() →
resolveOrAttachOrder() →
createPrescriptions() →
applyPricing() →
createPharmacyNotes() →
linkBackToStagingCase() + Rx_Image. On any
failure: ROLLBACK + Error_Log__c + structured error in body.
Either way, the sequence is: insert Account →
Health Cloud managed package auto-creates the paired Contact →
service / flow updates the Contact → insert EhrPatient +
ContactPointAddress → insert Order → insert up to 5 Master Rx
records (each with SF_Manually_Entered__c=true) →
downstream
New_Prescriptions_Manually_Entered flow creates
Fill + OrderItem + PricebookEntry → service applies prices
(sets Validation_Bypass_Date_Time__c to skirt
Prevent_Manual_Edits) → up to 3 Pharmacy_Notes →
new-order Case.
Three patient FKs on Rx must agree.HealthCloudGA__Patient__c,
HealthCloudGA__Account__c,
Contact__c
all point at the same human — no managed integrity helps here.
Drift = silent inconsistency.
Step 8 of 9
Auto-quarantine check + staging Case finalize
SalesforceCase_Insert_Update_Same_Recordnew-order Casestaging Case
On the new-order Case insert, the canonical Case BeforeSave
flow runs an auto-quarantine check:
does this patient have any open Case with RT=Quarantined_Order
or Refill_Request_Case?
If yes,
silently rewrite RT to Quarantined_Order and
OwnerId to Advanced Rx Admin user. Gated on ISNEW=true, so only Inserts trigger
the rewrite.
This is by design — patient with quarantine concerns gets
routed to admin, not CS. The service post-queries the Case and
returns the actual landed values
(actualCaseRecordType,
actualCaseOwnerType, wasQuarantined)
plus an AUTO_QUARANTINE_APPLIED warning so
consumers don't have to re-query.
Then the staging Case (the original Azure-created
Data_Input Case) is finalized:
ContactId + EHR_Practitioner__c set,
ParentId → new-order Case,
Status='Closed', Order__c set.
OwnerId deliberately left unchanged — avoids
tripping Restrict_Case_Ownership_Changes, the one
active VR on Case.
Two quarantine systems exist.Order.Skip_Quarantine_Check__c=true bypasses an
Order-side mechanism. It does NOT bypass this Case-side
auto-quarantine.
Step 9 of 9
PV1 pharmacist verifies
HumanPharmacistSalesforcePv1_Screen_Flow
A pharmacist verifies the prescription before any fill ships.
PV1 (Pharmacist Verification 1) is the regulatory checkpoint —
the actual gate between "data exists in Salesforce" and
"patient receives a fill."
PV1 is the same checkpoint regardless of who or what
produced the records.
Whether DE transcribed by hand, Phase 1+ AI pre-linked an
existing patient, or Phase 3's Apex service auto-wrote the
entire graph, every prescription passes through this
verification before it ships.
Out of scope for this plan — but the path
leads here. The whole upstream automation effort exists to
make PV1 faster, not to replace it.
Step 1 of 9
§9Hazards & gotchas
The subtle stuff: typo'd API names that mustn't be autocorrected,
hardcoded IDs to retrofit, silent-failure surfaces, multi-relationship
integrity, Health Cloud upgrade fragility, and a worth-keeping
teaching moment about inference vs. ground truth.
These API names are typo'd in the live org. The typos are
load-bearing — deployments referencing the "corrected" spelling
will fail with INVALID_FIELD. Don't autocorrect.
External_Id_c__c on Rx_Image__c —
_c__c suffix. UI label is
External_Id__c (clean). Phase 3 idempotent-upsert
path depends on this.
Date_filled__c on Master Rx — lowercase
d. Service writes this on every Rx insert.
Update_Prescription_image_via_Rx_Image_preocess —
preocess. It's an inactive Process Builder.
Overrided__c on Refill — should be
Overrode or Overridden.
Special_counseling_requirents_* on Compound —
requirents.
B2BAuthroize* Apex class family —
Authroize.
External_Id_c__c consequence: SOQL searches for
External_Id__c on Rx_Image__c miss
every Azure-staged record.
The screen flow + downstream
New_Prescriptions_Manually_Entered
flow together hardcode 5 RT IDs, 1 Group ID, and 1 Pricebook ID.
~175 locations across the org carry the same
anti-pattern. Phase A and B remove these from new code; Phase E
may opportunistically clean up untouched paths.
1 hardcoded Group ID —
CS_Case queue. Worse: stored as a flow constant
and pasted inline (the missed-dedup anti-pattern in
flow.md)
1 hardcoded Pricebook2 ID —
01s4W000005JCZUQA4 (Standard Price Book). Survives
same-parent sandbox refresh; breaks on cross-org migration.
The new service usesRecordType.DeveloperName /
Group.DeveloperName /
Pricebook2 WHERE Name='Standard Price Book'
exclusively.
Severity nuance (per
.claude/rules/apex.md): User IDs > Profile IDs
> RT IDs. User IDs break immediately on deactivation; RT IDs
survive same-parent refresh.
Five places in the pipeline can fail without surfacing a visible
error. Knowing where they are is half the battle when debugging.
Flow fault connectors org-wide: 18% conformance
per flow.md §3.12. Flows fired by Azure-written
Cases may fail silently.
Logic Apps Scope_Catch_Failure:
only triggers on HTTP error status codes. A 200-with-errors
response body is silently treated as success — every validation
failure would be lost. (This is why the Phase 3 endpoint must
use real HTTP status codes.)
No SF-side audit of Azure failures: the
Error_Log__c convention does not extend to
Azure-side failures. Debugging needs Azure run history + SQL +
Service Bus DLQ.
External_Id_c__c typo: SOQL on
External_Id__c for Rx_Image__c returns
zero Azure-staged records.
Azure connector field filtering: some managed
Salesforce connectors in Azure do client-side field filtering
and silently drop unknowns before the API call. Verify error
surfacing in sandbox before relying on it.
75% Apex coverage gate: measures executed
lines, not assertion density.
increaseCoverage() no-ops + zero-assertion tests
inflate reported coverage. Treat as a floor, not a quality
signal.
Master Rx carries three parallel patient-side relationships that
all must point at the same human. The Health Cloud package's
auto-pairing only protects one side of the triangle — the other
two are AdvancedRx-added custom Lookups with no managed integrity
guarantee.
If a future code path sets
HealthCloudGA__Patient__c to one patient's EhrPatient
but Contact__c to a different patient's Contact, the
data is silently inconsistent — none of the Health-Cloud-managed
integrity helps. The service must resolve the
Account+Contact+EhrPatient triple as a
single unit and populate all three FKs from that
single resolution result. (Phase A0 §1 documents the screen flow's
correct multi-FK population.)
The service writes to managed HealthCloudGA__*
objects (Account-side via auto-Contact-create, plus EhrPatient,
Master Rx, Fill). Future package upgrades may rename managed
fields and silently break the service.
Pin Health Cloud version in the test plan.
Rerun service tests after every HC managed-package upgrade.
7 VRs on managed
HealthCloudGA__* objects
(Rx, Practitioner) carry custom AdvancedRx logic —
upgrade-fragile too. Any package upgrade may invalidate the
bypass logic.
The auto-Contact-create dependency is load-bearing.
Inserting an Account (Individual RT, Group + NonDual picklist
values) triggers HC-managed-package code that creates the paired
Contact server-side. The whole 5-step patient-creation chain
depends on this. If HC behavior changes, the service breaks
silently.
Phase A test gate: verify auto-Contact-create
works under with sharing Apex running as the
integration user before Phase A ships. If it fails, three
options — widen the integration user's perms, flip the relevant
Account-insert step to without sharing, or abandon
the auto-create path entirely.
The Azure pipeline story has been corrected twice
during this project's research. Both corrections illustrate the
same hazard: structural inference produces internally consistent
but externally wrong attributions with unsettling ease.
2026-04-17
Inference: the 5 *_to_Salesforce_*__mdt mapping
CMDTs target the LifeFile pipeline.
Wrong
Reasoning shape: CMDT naming pattern + LifeFile is real → must
be the target. But LifeFile has zero SF-side integration.
2026-04-19
Replacement inference: the same 5 CMDTs feed
the Azure inbound flow.
Also wrong
Reasoning shape: Azure is real + CMDTs must feed something →
must be the target. Azure uses none of these CMDTs; grep found
references only in dead NewLeaf batch jobs.
2026-04-21
Confirmed: the 5 CMDTs are
NewLeaf-era legacy artifacts — die with the
NewLeaf cleanup. Independent of Azure entirely.
Current
Lesson worth keeping: internal-shape inference
(object models, naming conventions, trigger frameworks, junction
patterns, audit-field shapes) is reliable.
External-system identity and data-flow topology from metadata
alone are not.
"Not A, therefore B" isn't confirmation of B — it's a narrower
inference with the same shape, and the same failure mode. When a
CMDT, code class, or field name suggests "this connects to System
X," treat it as a hypothesis until human-confirmed.
§10Open questions
What still needs a decision before each phase ships. Border colors:
Kyle decisionMarissa neededSandbox-verifiableRecommended (not blocking).
Q1 · Phase 3 perm-set blast radius
Kyle decision
Accept consolidation on the existing
Azure Logic Apps Connected App +
Azure_Integration perm set (continues the Phase 0/1/2
trajectory)? Or split a separate Connected App + perm set for
Phase 3 writes only (isolates the write blast radius at
rotation-coordination cost)?
Blocks: Phase D cutoverOwner: KyleReference:plan.md §6 risk
#6
Q2 · Fax intake JSON contract
Kyle + Marissa
Does Azure parse faxes into the same domain JSON contract as
e-scripts (one endpoint, one schema), or do faxes use a different
contract / a different channel? Initial Phase D cutover keeps
faxes on the existing image-staging path, so this can be deferred
— but must be answered before the fax row of
Intake_Pipeline_Routing__mdt flips to
true.
Blocks: Phase D fax rowOwner: Kyle + MarissaReference:plan.md §9 #2
Q3 · Confidence-threshold tuning
Kyle (post-launch)
At what Azure-AI confidence floor does the structured-write path
stop being worth it (records bouncing at PV1 too often, parser
ambiguity outweighing DE-team labor savings)? Tunable Azure-side
via the AI Search threshold + the per-channel CMDT —
not a Phase D blocker; tune from real-world match
quality after launch.
Azure never sets this checkbox (verified via
wf-sf-writer). The
DE-team-sets-it-true-for-manual-uploads interpretation is
supported by code + workflow history, but Marissa's explicit
confirmation is still pending. Affects nothing in this plan
directly — flagged here so future code touching the field knows
the semantics aren't fully confirmed.
Blocks: nothing in this planOwner: MarissaReference:azure-integration.md open question #2
Q5 · Phase 1 DE workflow redesign
Marissa decision
When RequirePatientMatching flips, DE's workflow
changes — some Cases arrive pre-linked, some don't. Kyle's leading
idea: encode confidence + match status in the Case subject so DE
can quickly assess whether to trust the auto-link. DE keeps human
review either way; the goal is reduced lookup time, not removed
review. Marissa decides the Case-subject format convention.
Blocks: Phase 1 prod cutoverOwner: MarissaReference:azure-integration.md open question #3
Q6 · SF-side match audit trail
Recommended
Today the AI confidence score lives in Azure SQL only (dbo.PrescriptionDocuments.PatientMatchScore
+ PatientMatchReasons). A pharmacist debugging "why
did this Case link to the wrong patient?" needs Azure SQL access.
Recommended before Phase 1 prod cutover: add
Case.Match_Confidence_Score__c +
Case.Match_Reasons__c; wire
wf-sf-writer +
usp_UpdateSalesforceStatus to populate. One-line
Azure change; SF additions carry the 12-flow + 21-RT blast radius
— coordinate the deploy.
Confirm the Azure Logic Apps Connected App consumer
secret is tracked in Azure Key Vault with expiry-alert wiring
(analogous to
MICROSOFT_PROVIDER_AUTHENTICATION_SECRET). Extend
task-13-secret-rotation.md to cover the bidirectional
refresh (ADF LS_Salesforce_Production
+ inbound Logic Apps auth). Close before Phase 1 prod cutover.
Blocks: Phase 1 prod cutoverOwner: Kyle (verifiable in Azure Portal)Reference:azure-integration.md open question #5
Q8 · Health Cloud auto-Contact-create under
with sharing
Sandbox-verifiable
Phase A authoring depends on Health Cloud's managed-package code
creating the paired Contact when the integration user inserts an
Account (Individual RT, Group + NonDual). The screen flow runs
SystemModeWithoutSharing. Whether the auto-create
works the same when invoked from with sharing Apex is
unverified. Phase A gate — if the test fails, the
team picks between widening the integration user's perm set,
flipping the Account-insert step to without sharing,
or abandoning the auto-create path (changes the contract).
Foreign Key — Salesforce Lookup or Master-Detail relationship.
FLS
Field-Level Security — per-field read/edit permission, layered on
the perm set.
HC
Health Cloud — Salesforce managed package providing
HealthCloudGA__* objects.
LWC
Lightning Web Component.
NC
Named Credential — SF surface for secure outbound callout
configuration.
NPI
National Provider Identifier — 10-digit prescriber ID (3 active VRs
enforce format).
OWD
Organization-Wide Default — SF base sharing model (Public
Read/Write, Read Only, or Private).
PAD
Power Automate Desktop — Microsoft's desktop UI-automation tool.
Used to scrape LifeFile.
PB
Process Builder — legacy SF automation tool, superseded by Flow.
PHI
Protected Health Information — under USP 795 + HIPAA scope at
AdvancedRx.
PV1
Pharmacist Verification 1 — regulatory checkpoint before any fill
ships. Same checkpoint regardless of who produced the records.
RT
Record Type — drives layouts, picklist values, and automation
routing.
SCV
Service Cloud Voice — Salesforce voice-call lifecycle product (30
active seats, Amazon Connect routing).
SOQL
Salesforce Object Query Language — SF's read-only query language.
USP 795
United States Pharmacopeia chapter 795 — governs nonsterile
compounding. AdvancedRx is licensed in 48 continental states.
VR
Validation Rule — record-level field validation; fails the DML if
the formula returns true.
API names that recur
azure-integration@advancedrx.net
The Run-As user for both inbound Logic Apps writes and outbound ADF
reads. CreatedById on every Azure-written record.
Azure Logic Apps
The Connected App fronting both Azure workloads. Note: same name as
the Azure service — confusing. Connected App ID in ClaudeTest:
0H4aZ00000013OrSAI.
Azure_Integration
The perm set layered on the integration user. Each phase expands its
scope.
Case_Insert_Update_Same_Record
Canonical Case BeforeSave flow. The auto-quarantine override (RT +
Owner rewrite when patient has open quarantined or refill-request
Case) lives here, gated on ISNEW=true.
Escript.Evaluator.RequirePatientMatching
Phase 1 feature flag (Azure-side). False today; flipping to true
triggers AI patient-matching and pre-populates
Case.ContactId on high-confidence matches.