Shipped (2026-06): the seam is live —@/platform/workflowexposesenqueueWorkflowExecution/getThrottleStatus(src/platform/workflow/execution.ts) over thefw-enqueue-executionedge function, with theuseWorkflowEnqueuehook for component use.
Context
The FW completion audit flagged IG-06 as the keystone cross-core blocker: there is no sanctioned platform seam for HR/FA/RH to enqueue an FW workflow execution or read FW throttle/KPI status. As a result the three FW integration guides are frozen at📝 Planned and 0 of 19 cross-core workflow templates exist
(6 HR + 5 FA + 6 RH “Planned Workflow Templates” tables in
HR-FW-INTEGRATION.md,
FA-FW-INTEGRATION.md,
RH-FW-INTEGRATION.md).
Current state (file:line evidence)
@/platform/workflowis diagram-only.src/platform/workflow/index.ts:1-42exportsSwimLaneCanvas,WorkflowCanvas, diagram version/presence/broadcast hooks and BPMN utils — no execution API.src/platform/workflow/types.tsis entirely swim-lane/canvas types; there is no execution request/result type.- FW’s real execution path is pgmq-based. The durable worker
supabase/functions/workflow-executor-worker/index.tsreadsworkflow_execution_queueviapgmq_read(line ~568), processes batches under a per-org semaphore onfw_module_settings(lines ~110-150), and on permanent failure routes toworkflow_dlqviapgmq_send. The canonical enqueue primitive isINSERT INTO fw_workflow_executions (… status: 'queued' …)followed bysupabase.rpc('pgmq_send', { queue_name: 'workflow_execution_queue', msg: { execution_id, rule_id, trigger_type, organization_id, attempt } })— see the recovery-workflow path atsupabase/functions/workflow-executor-worker/index.ts:490-519for the exact shape. - App-side cores never touch pgmq directly today — they invoke an edge function
(e.g.
automation-executor,cl-loc-assessment-get) and let the function own the insert+enqueue. The cross-core service style to mirror issrc/platform/clinical/loc/getLocAssessmentForUM.ts— a thin async function thatsupabase.functions.invoke(...)s an org/consent-gated edge function and returns a typed result. - FW-53 throttle is live.
fw_rate_limit_configs/fw_execution_rate_counters(types:src/cores/fw/types/rate-limiting.ts) back a per-org / per-workflow rate state; the read hook issrc/cores/fw/hooks/useExecutionRateSnapshot.ts. The worker rate-limit enforcement (B3) defers/holds over-limit work rather than dropping it (conflict_resolution: 'skip' | 'delay' | 'error'insupabase/functions/_shared/fw-schedule-dispatch.ts:22). A cross-core caller needs to see that hold state before/while enqueuing. - FW-58 KPI snapshots live in
fw_kpi_snapshots(src/cores/fw/hooks/useKpiSnapshots.ts).
The boundary constraint
Constitution §1 / ADR-010: cores depend on PF only; no direct core-to-core imports. HR/FA/RH must not import fromsrc/cores/fw/*.
The only sanctioned channel is the Platform Integration Layer (@/platform/*).
Decision
Add an execution seam to@/platform/workflow — a small, typed, tenant-scoped public
API that wraps the FW pgmq enqueue path behind an edge function, plus read surfaces for
throttle and KPI state. Cross-core callers (HR/FA/RH) import only from
@/platform/workflow; FW owns the edge function and the queue.
Under the hood the enqueue does NOT write pgmq from the browser. It calls a new FW
edge function (working name fw-enqueue-execution) that, with the service role, runs
pf_has_org_access → idempotency lookup → INSERT fw_workflow_executions (status:'queued')
→ pgmq_send('workflow_execution_queue', …) inside one transaction. This keeps RLS,
tenant checks, and the queue contract on the FW/PF side and gives the platform layer a
boundary-clean wrapper.
Proposed API (@/platform/workflow)
@/platform/clinical):
enqueueWorkflowExecution/getThrottleStatusship as plain async services (callable from mutations, schedulers, or React) — thegetLocAssessmentForUMpattern.- A thin
useWorkflowEnqueue()mutation hook anduseWorkflowThrottleStatus()/useWorkflowKpiSnapshot()query hooks wrap them for component use (theuseExecutionRateSnapshot/useKpiSnapshotspattern), reusinguseOrganization()for the default org andsanitizeErrorMessagefor errors.
Tenant + auth scoping
organizationIdis always re-verified server-side in the edge function viapf_has_org_access; client-supplied org is never trusted (security rule + §1).- Caller must hold a workflow-execute permission for the org (new
fw.workflows.execute_cross_corepermission, checked in the edge fn). Read surfaces (getThrottleStatus,subscribeKpiSnapshot) are RLS-scoped reads offw_execution_rate_counters/fw_kpi_snapshots— no new write capability. - No PHI in
triggerPayload,workflowKey, orcorrelationId(security rule); the edge fn rejects payloads exceeding a size cap and logs only IDs.
Idempotency
idempotencyKey (caller-chosen, e.g. hr_employee_hired:{employeeId}) is persisted on
fw_workflow_executions under a UNIQUE (organization_id, idempotency_key) partial index.
The edge fn does an upsert-style lookup: a second enqueue with the same key returns the
existing executionId with status:'deduplicated' and does not re-pgmq_send. This
makes retries from HR/FA/RH (and event redelivery) safe.
Failure modes & backpressure
How HR/FA/RH consume it
📝 Planned to ✅ Implemented once the seam + per-core
template workflowKeys are registered.
Phased implementation plan
- Phase 0 — this ADR (planning). No code. ~done.
- Phase 1 — MVP (
enqueueWorkflowExecution+getThrottleStatus). Newfw-enqueue-executionedge fn (insert+pgmq_send+idempotency+pf_has_org_access); migration addingidempotency_key+ unique index +fw.workflows.execute_cross_corepermission; the two async services + auseWorkflowEnqueuehook insrc/platform/workflow/; RLS + integration tests; wire one template end-to-end (hr-employee-onboarding) as proof. Est. 3–4 days. - Phase 2 — KPI subscription + remaining read surface.
subscribeKpiSnapshotover Supabase Realtime onfw_kpi_snapshots; query hooks; docs. Est. 1–2 days. - Phase 3 — roll out the 19 templates across HR/FA/RH, flipping each integration guide to Implemented. Est. 1–2 days per core (template-config heavy, not seam work).
Open questions (for human decision)
workflowKey→rule_idresolution. Per-org registry table (fw_workflow_template_registry) vs. convention onfw_automation_rules.template_key? Affects whether each org must “install” a template before cross-core enqueue works.- Throttled = accepted-or-rejected? Should
enqueueWorkflowExecutionalways persist the row on FW-53 hold (return'throttled', drain later) or hard-fail so the caller owns retry? MVP assumes accept-and-hold; confirm this is the desired backpressure contract. - Permission model. New
fw.workflows.execute_cross_core, or reuse an existing FW-03 automation-trigger permission? Does an HR user enqueueing an FW workflow need an FW grant, or is the cross-core call system-actor on the user’s behalf? - Edge fn vs. PF RPC. Wrap via a Deno edge function (matches
getLocAssessmentForUM) or a singleSECURITY DEFINERPostgres function (fw_enqueue_execution) the seam calls viasupabase.rpc? RPC is one less deploy surface but moves pgmq_send into SQL. - KPI realtime vs. poll. Is
fw_kpi_snapshotsupdated often enough to justify a Realtime channel, or is a polling query hook (likeuseKpiSnapshots) sufficient for Phase 2?
Consequences
@/platform/workflowgains a second responsibility (execution) alongside diagrams; index doc should section it clearly.- FW owns one new edge fn + one additive migration; no change to the worker contract.
- HR/FA/RH stay import-clean (boundary preserved); the 19 templates become buildable.
- New always-on surface is minimal (small typed API; tree-shakeable).
References
- ADR-010 Core–PF Dependency Boundary
- ADR-004 CL↔FW Event Patterns
- HR-FW · FA-FW · RH-FW integration contracts
supabase/functions/workflow-executor-worker/index.ts(pgmq enqueue/dispatch shape)src/platform/clinical/loc/getLocAssessmentForUM.ts(cross-core service pattern)- FW-53 rate limiting (
src/cores/fw/types/rate-limiting.ts) · FW-58 KPI (src/cores/fw/hooks/useKpiSnapshots.ts)