Summary
Intake depends on coordination across several systems: claiming a task should open the application for review, submitting an application should generate the required document checklist and kick off verification, and results from verification services, appointment scheduling, and eligibility determinations should flow back to the application automatically. Without this wiring, caseworkers have to update application records by hand after each external action — re-entering data the system already has.
This issue implements the event subscriptions that drive that automation. Rule sets react to events from workflow, data exchange, scheduling, and document management and update application state without caseworker intervention. It also adds POST /platform/events to the mock server so forward subscriptions (events from domains not yet built) can be injected and exercised end-to-end.
It also adds API paths for ApplicationDocument and Interview — entities created by the rules engine but previously unqueryable — and updates the path structure to use sub-resource paths where appropriate, with explicit conventions added to api-patterns.yaml.
This is the implementation issue for the event contracts decisions in #232.
Use cases
- As a caseworker, I want the system to automatically open my review when I claim a task, so I don't have to open the application manually in a separate step
- As a caseworker, I want document checklists to be automatically created when an application is submitted, so I don't have to manually request income, identity, and residency documents for each applicant
- As a caseworker, I want the application to reflect the results of identity, income, and citizenship verification automatically, so I don't have to re-enter information that was already checked electronically
- As a caseworker, I want eligibility determination results to be recorded on the application automatically, so I can see the outcome of each program check without switching systems
- As a supervisor, I want interview completion and document verification to be recorded on the application automatically, so caseworkers don't have to update the record manually after each step
Scope
Event subscriptions and rule sets:
packages/contracts/intake-rules.yaml — add rule sets for each subscription (declared via on: in the rules file)
packages/contracts/schemas/rules-schema.yaml — add all-match to evaluation enum; allow from: to accept JSON Logic {var: "..."} form
packages/mock-server/scripts/server.js — implement POST /platform/events wired to the event bus so externally injected events fire subscriptions
Application review trigger:
packages/contracts/intake-openapi.yaml — add application.review_completed to x-events with payload schema
packages/contracts/intake-state-machine.yaml — add complete-review trigger (from: under_review, no to: — stays in current state, emits application.review_completed per Decision 6)
Sub-resource paths for ApplicationDocument and Interview:
packages/contracts/intake-openapi.yaml — add /applications/{applicationId}/documents (collection + item) and /applications/{applicationId}/interview (singleton); transitions come from state machine contracts via overlay
packages/mock-server/src/route-generator.js — update deriveCollectionName and endpoint type detection to handle nested sub-resource paths; add singleton sub-resource handling for Interview
docs/architecture/domains/intake.md — update Decision 20 to reflect sub-resource paths and singleton Interview pattern
API pattern conventions:
packages/contracts/patterns/api-patterns.yaml — add explicit patterns for sub-resource paths (noun segments, owned child entities with lifecycle) and RPC transition endpoints (verb segments); clarify the noun/verb distinction
Subscriptions (declared in intake-rules.yaml via on:):
| Event |
Source |
Effect |
Status |
workflow.task.claimed |
Workflow |
Transition application submitted → under_review; create Interview record |
Active |
intake.application.submitted |
Intake |
Create document checklist per applicant programs (Decision 16) |
Active |
data-exchange.service-call.completed |
Data Exchange |
Write back FDSH/IEVS/SAVE/SSA results; create citizenship doc if FDSH inconclusive |
Forward |
scheduling.appointment.scheduled |
Scheduling |
Append appointment ID to Interview.appointments (Decision 15) |
Forward |
document-management.document.verified |
Document Management |
Transition ApplicationDocument to verified |
Forward |
eligibility.determination.completed |
Eligibility |
Write determination outcome to ApplicationMember (informational write-back only) |
Forward |
How to validate
Schema validation:
Expected: ✓ All schema validations passed!
workflow.task.claimed — start the mock server (npm run mock:start), then:
APP_ID=$(curl -s -X POST http://localhost:1080/intake/applications \
-H "Content-Type: application/json" \
-H "X-Caller-Id: user-1" -H "X-Caller-Roles: applicant" \
-d '{"programs": ["snap"], "channel": "online"}' | jq -r .id)
curl -s -X POST "http://localhost:1080/intake/applications/$APP_ID/submit" \
-H "X-Caller-Id: user-1" -H "X-Caller-Roles: applicant"
TASK_ID=$(curl -s "http://localhost:1080/workflow/tasks?subjectId=$APP_ID" \
-H "X-Caller-Id: user-1" -H "X-Caller-Roles: caseworker" | jq -r '.items[0].id')
curl -s -X POST "http://localhost:1080/workflow/tasks/$TASK_ID/claim" \
-H "X-Caller-Id: user-1" -H "X-Caller-Roles: caseworker"
curl -s "http://localhost:1080/intake/applications/$APP_ID" \
-H "X-Caller-Id: user-1" -H "X-Caller-Roles: caseworker" | jq .status
# → "under_review"
Document checklist on submit — using the APP_ID from above:
curl -s "http://localhost:1080/intake/applications/$APP_ID/documents" \
-H "X-Caller-Roles: caseworker" | jq '[.items[].category]'
# → ["income", "identity", "residency"] (SNAP applicant — three documents requested on submit)
complete-review trigger:
curl -s -X POST "http://localhost:1080/intake/applications/$APP_ID/complete-review" \
-H "X-Caller-Id: user-1" -H "X-Caller-Roles: caseworker" | jq .status
# → "under_review"
Forward subscriptions via POST /platform/events — use the IDs from the steps above:
DOC_ID=$(curl -s "http://localhost:1080/intake/applications/$APP_ID/documents" \
-H "X-Caller-Roles: caseworker" | jq -r '.items[0].id')
INTERVIEW_ID=$(curl -s "http://localhost:1080/intake/applications/$APP_ID/interview" \
-H "X-Caller-Roles: caseworker" | jq -r .id)
# document-management.document.verified → transitions ApplicationDocument to verified, populates evidence
curl -s "http://localhost:1080/intake/applications/$APP_ID/documents/$DOC_ID" \
-H "X-Caller-Roles: caseworker" | jq .status
# → "requested"
curl -s -X POST http://localhost:1080/platform/events \
-H "Content-Type: application/json" \
-d "{\"specversion\":\"1.0\",\"type\":\"org.codeforamerica.safety-net-blueprint.document-management.document.verified\",\"source\":\"/document-management\",\"id\":\"evt-doc\",\"subject\":\"dm-doc-abc123\",\"data\":{\"subjectType\":\"application-document\",\"subjectId\":\"$DOC_ID\"}}" | jq .id
curl -s "http://localhost:1080/intake/applications/$APP_ID/documents/$DOC_ID" \
-H "X-Caller-Roles: caseworker" | jq '{status, evidence}'
# → {"status": "verified", "evidence": ["dm-doc-abc123"]}
# subject in the CloudEvents envelope is stored in evidence; subjectId in data is used for lookup
# data-exchange.service-call.completed with fdsh+inconclusive → creates citizenship ApplicationDocument
curl -s -X POST http://localhost:1080/platform/events \
-H "Content-Type: application/json" \
-d "{\"specversion\":\"1.0\",\"type\":\"org.codeforamerica.safety-net-blueprint.data-exchange.service-call.completed\",\"source\":\"/data-exchange\",\"id\":\"evt-fdsh\",\"subject\":\"svc-1\",\"data\":{\"applicationId\":\"$APP_ID\",\"serviceType\":\"fdsh\",\"result\":\"inconclusive\",\"verificationType\":\"citizenship\"}}" | jq .id
curl -s "http://localhost:1080/intake/applications/$APP_ID/documents" \
-H "X-Caller-Roles: caseworker" | jq '[.items[].category]'
# → includes "citizenship"
# scheduling.appointment.scheduled → appends appointment ID to Interview.appointments
curl -s -X POST http://localhost:1080/platform/events \
-H "Content-Type: application/json" \
-d "{\"specversion\":\"1.0\",\"type\":\"org.codeforamerica.safety-net-blueprint.scheduling.appointment.scheduled\",\"source\":\"/scheduling\",\"id\":\"evt-appt\",\"subject\":\"appt-abc123\",\"data\":{\"subjectType\":\"interview\",\"subjectId\":\"$INTERVIEW_ID\"}}" | jq .id
curl -s "http://localhost:1080/intake/applications/$APP_ID/interview" \
-H "X-Caller-Roles: caseworker" | jq .appointments
# → ["appt-abc123"]
# eligibility.determination.completed → informational write-back only (app stays under_review)
curl -s -X POST http://localhost:1080/platform/events \
-H "Content-Type: application/json" \
-d "{\"specversion\":\"1.0\",\"type\":\"org.codeforamerica.safety-net-blueprint.eligibility.determination.completed\",\"source\":\"/eligibility\",\"id\":\"evt-elig\",\"subject\":\"$APP_ID\",\"data\":{\"applicationId\":\"$APP_ID\",\"program\":\"snap\",\"outcome\":\"approved\",\"determinedAt\":\"2026-04-16T00:00:00Z\"}}" | jq .id
curl -s "http://localhost:1080/intake/applications/$APP_ID" \
-H "X-Caller-Id: user-1" -H "X-Caller-Roles: caseworker" | jq .status
# → "under_review" (application does not close automatically; close is a manual caseworker action)
Dependencies
Related
Summary
Intake depends on coordination across several systems: claiming a task should open the application for review, submitting an application should generate the required document checklist and kick off verification, and results from verification services, appointment scheduling, and eligibility determinations should flow back to the application automatically. Without this wiring, caseworkers have to update application records by hand after each external action — re-entering data the system already has.
This issue implements the event subscriptions that drive that automation. Rule sets react to events from workflow, data exchange, scheduling, and document management and update application state without caseworker intervention. It also adds
POST /platform/eventsto the mock server so forward subscriptions (events from domains not yet built) can be injected and exercised end-to-end.It also adds API paths for
ApplicationDocumentandInterview— entities created by the rules engine but previously unqueryable — and updates the path structure to use sub-resource paths where appropriate, with explicit conventions added toapi-patterns.yaml.This is the implementation issue for the event contracts decisions in #232.
Use cases
Scope
Event subscriptions and rule sets:
packages/contracts/intake-rules.yaml— add rule sets for each subscription (declared viaon:in the rules file)packages/contracts/schemas/rules-schema.yaml— addall-matchto evaluation enum; allowfrom:to accept JSON Logic{var: "..."}formpackages/mock-server/scripts/server.js— implementPOST /platform/eventswired to the event bus so externally injected events fire subscriptionsApplication review trigger:
packages/contracts/intake-openapi.yaml— addapplication.review_completedtox-eventswith payload schemapackages/contracts/intake-state-machine.yaml— addcomplete-reviewtrigger (from: under_review, noto:— stays in current state, emitsapplication.review_completedper Decision 6)Sub-resource paths for ApplicationDocument and Interview:
packages/contracts/intake-openapi.yaml— add/applications/{applicationId}/documents(collection + item) and/applications/{applicationId}/interview(singleton); transitions come from state machine contracts via overlaypackages/mock-server/src/route-generator.js— updatederiveCollectionNameand endpoint type detection to handle nested sub-resource paths; add singleton sub-resource handling for Interviewdocs/architecture/domains/intake.md— update Decision 20 to reflect sub-resource paths and singleton Interview patternAPI pattern conventions:
packages/contracts/patterns/api-patterns.yaml— add explicit patterns for sub-resource paths (noun segments, owned child entities with lifecycle) and RPC transition endpoints (verb segments); clarify the noun/verb distinctionSubscriptions (declared in intake-rules.yaml via
on:):workflow.task.claimedsubmitted→under_review; create Interview recordintake.application.submitteddata-exchange.service-call.completedscheduling.appointment.scheduledInterview.appointments(Decision 15)document-management.document.verifiedeligibility.determination.completedHow to validate
Schema validation:
Expected:
✓ All schema validations passed!workflow.task.claimed — start the mock server (
npm run mock:start), then:Document checklist on submit — using the APP_ID from above:
complete-review trigger:
Forward subscriptions via POST /platform/events — use the IDs from the steps above:
Dependencies
on:field format used in intake-rules.yaml rule sets)Related
service-call.completedevents intake subscribes to)