Plan: SNAP + TANF + ELE end-to-end demo video — full simple-case workflow
On this page
Status
| Step | Description | Status |
|---|---|---|
A1 |
Staged demo archetype: 2-person household (Maria Lopez + 1 child), |
Not started |
A2 |
9-step workflow stepper component (visual progress indicator at top of intake pages) |
Not started |
A3 |
New |
Not started |
A4 |
Per-program work queue split (SNAP queue vs TANF queue) — visible affordance for independence demo |
Not started |
B1 |
Section: Initiate Review (kickoff state, audit event) |
Not started |
B2 |
Section: Applicant Group + Applicant Group Address (household composition + address) |
Not started |
B3 |
Section: Persons + Person Demographics (DOB, SSN, gender, race, ethnicity) |
Not started |
B4 |
Section: Authorized Representative (capture / N/A) |
Not started |
B5 |
Section: Primary Individual + Individual Interviewed (HoH designation + interview attestation) |
Not started |
B6 |
Section: ELE Consent (capture worker-attested consent; per-child opt-in) |
Not started |
B7 |
Section: Program Request (which programs being requested + verification) |
Not started |
B8 |
Section: Relationship (intra-household relationships) |
Not started |
B9 |
Section: Education (per-person education status) |
Not started |
B10 |
Section: Living Arrangement (housing situation, owned/rented, with whom) |
Not started |
B11 |
Section: Work Number / ABAWD Activity / Work Registration (work-eligibility status — SNAP-specific) |
Not started |
B12 |
Step 7: Complete Data Collection — validation pass, transition state from |
Not started |
B13 |
Step 9: Complete Case (SNAP) — runs determination, generates NOA |
Not started |
C1 |
TANF intake page shell — reuses |
Not started |
C2 |
TANF-specific section: Non-Compliance / Sanction tracking (PAMMS 1351) |
Not started |
C3 |
TANF-specific section: Time Limits (60-month federal lifetime; state extensions) |
Not started |
C4 |
TANF-specific section: Personal Responsibility Plan (PRP attestation) |
Not started |
C5 |
TANF-specific section: Family Cap (state-specific; capture even if not applied) |
Not started |
C6 |
Step 9: Complete Case (TANF) — runs TANF determination, generates NOA |
Not started |
D1 |
Per-program work-queue split visible — SNAP worker view vs TANF worker view as separate panels |
Not started |
D2 |
Audit-tab visibility for both programs' parallel processing (no cross-contamination of audit chains) |
Not started |
D3 |
Side-by-side state demonstration: SNAP determined ≠ TANF determined, no auto-fill |
Not started |
E1 |
ELE status table (per-child: |
Not started |
E2 |
ELE subscriber on |
Not started |
E3 |
ELE renewal logic: at renewal day, check if any source program still approved; if so, extend; if none, expire |
Not started |
E4 |
ELE status visible on case-detail (badge + expiration date + granting history) |
Not started |
F1 |
Dry-run recording with |
Not started |
F2 |
Polish pass on visual gotchas surfaced during dry-run |
Not started |
F3 |
Final recording |
Not started |
Epic: TBD (request to be filed after this plan ratifies)
Branch: feat/snap-tanf-ele-demo-video
Target: Recorded 10-minute end-to-end deep-dive demo video showing SNAP + TANF independence + automatic ELE flag
Context
After the 2026-05-27 internal demo, stakeholders asked for a recorded follow-up showing the complete worker experience for a single multi-program case. Caseworker SME feedback then materially reshaped the scope:
-
Program independence is the headline demo objective. "The big thing you must display is that SNAP and TANF can be worked separately and they do not impact each other. This is the number one reason for being behind in work right now." (Caseworker SME, 2026-05-27). Different workers touch different programs; an ABD specialist works the ABD-attached portion while a SNAP worker handles SNAP — they shouldn’t block each other.
-
ELE is a status flag, not a cross-service application cascade. Original plan had ELE wrong. Correct semantics per SME:
-
ELE is granted to children in the household for 1 year when any of
{SNAP, TANF, CAPS, WIC}is approved (hierarchy isSNAP > TANF > CAPS > WIC, but any single approval is sufficient). -
SNAP approval is independent of TANF approval — they’re separate decisions. If SNAP is approved and TANF is denied, the kids still get ELE.
-
ELE renewal: if any of the 4 source programs is still approved when ELE comes due, ELE auto-renews for another year. ELE only actually expires when all source programs lapse simultaneously.
-
ELE Consent is captured upstream as a worker action (per-child opt-in).
-
-
The workflow has 9 steps and ~75 data-collection areas total. A simple two-person household with no income/resources/expenses/interfaces/docs/appointment/verification touches ~15 of them and runs through 5 of the 9 steps (1 → 2 → 3 → 7 → 9, skipping 4/5/6/8). This is the case the video demonstrates. The complex case (35 areas, 9 full steps, ~85 minutes real-world) is out of scope for the video.
Related: ADR-002 (black-box determination contract), ADR-003 (rulesets-as-data), #600 (per-program case numbers — deferred), #602 (client-level data isolation across programs), #605 (unified trading-partner framework), #607 (work tracking + assignment revamp).
Scope
In scope:
-
New
/applications/{id}/intakepage with a 9-step workflow stepper and an accordion-style data-collection layout -
15-16 SNAP intake data-collection sections covering the simple-case workflow (per the 2026-05-27 SME-supplied workflow diagram)
-
~5 TANF-specific additional sections (Non-Compliance, Time Limits, PRP, Family Cap)
-
Per-child ELE Consent capture
-
Per-program work-queue split visible on the dashboard (independence demonstration)
-
ELE status table + subscriber + renewal logic (architecturally honest, but simpler than the previous event-cascade misread)
-
A staged single 2-person household at
submittedstate for recording -
cargo xtask seed --profile demo --resetis the canonical between-takes reset
Out of scope:
-
The 60+ data-collection sections the complex case touches (income, resources, expenses, ABAWD activity history, medical, disability, etc.). Simple-case-only.
-
Steps 4, 5, 6, 8 of the 9-step workflow (Appointment / Documents / Interfaces / Verification Checklist). Simple case skips these by definition (no income, no docs, no interfaces, no verification needed).
-
Scripted IEVS / SAVE / SSA SOLQ adapters — simple case has no interface results, so no fake-partner adapters needed for this video.
-
CAPS + WIC source programs for ELE. Both are real source programs in the hierarchy, but the demo focuses on SNAP + TANF. The ELE subscriber will be coded to handle all 4 (so adding CAPS/WIC later is a config change, not a code change), but the demo only exercises SNAP and TANF.
-
CHIP coverage. ELE applies to Medicaid AND CHIP separately; the demo focuses on Medicaid-side ELE only.
-
Real partner connectivity (IEVS / SAVE / SSA / FFE). All adapters stay at their current Noop state.
-
Per-program case numbers (#600). Demo uses today’s household-derived case-number presentation.
Design
The 10-minute demo script (revised)
| Time | What the viewer sees | What’s happening behind the scenes |
|---|---|---|
0:00–0:30 |
Title card: "canopy: SNAP + TANF + ELE — demonstrating program independence" |
Static |
0:30–1:00 |
Dashboard login. The presenter logs in as |
Per-program work-queue split: |
1:00–1:30 |
Presenter opens the SNAP case → |
|
1:30–4:00 |
Presenter walks through the data-collection accordion: Initiate Review, Applicant Group, Applicant Group Address, Persons (2 rows: Maria + 1 child), Person Demographics (DOB / SSN / etc), Authorized Representative (N/A), Primary Individual (Maria HoH), Individual Interviewed (Maria), ELE Consent (per-child opt-in — Maria consents on behalf of the child), Program Request (SNAP requested), Relationship (Maria → biological mother of child), Education (Maria HS grad, child preschool), Living Arrangement (renting, household alone), Work Number / ABAWD Activity / Work Registration (Maria registered, not ABAWD). |
Sections |
4:00–4:30 |
Presenter clicks "Complete Data Collection" (Step 7). Validation runs, application state transitions |
|
4:30–5:00 |
Presenter clicks "Run Determination" (Step 9). SNAP determination fires, returns ✓ ELIGIBLE — APPROVE, $234/mo (simple case, no income → max allotment). NOA generated, visible in Notices tab within ~1s. |
|
5:00–5:30 |
ELE flag turns on automatically. Case-detail view now shows an ELE badge: "Children: ELE active (1 year, granted by SNAP, expires 2027-05-27)." Audit tab shows the new event chain: |
|
5:30–6:00 |
Independence demonstration. Presenter pivots: switches to |
|
6:00–8:00 |
Presenter walks TANF intake ( |
|
8:00–8:30 |
Presenter completes TANF Step 7 and Step 9. TANF determination runs. ✓ ELIGIBLE — APPROVE. TANF NOA generated. |
|
8:30–9:00 |
ELE status updates. Audit tab now shows a second |
|
9:00–9:30 |
Audit tab — presenter walks the full chain: separate audit rows for SNAP determination, TANF determination, two ELE grant events. Hash chain verification visible inline (✓ JWS verified). Presenter notes: "The SNAP and TANF audit chains never cross. Each program has its own causal record." |
|
9:30–10:00 |
Cross-program summary view: SNAP ✓ Active, TANF ✓ Active, ELE active for children. Close. |
Static recap. |
Independence model (the architectural story)
The video’s headline demonstration is that SNAP and TANF can be worked separately, by different workers, without one program’s state leaking into the other. Architecturally this is already true (per-service databases, ADR-001 + ADR-004), but the worker portal has been showing them in a unified view that obscures the separation. The work in this plan exposes the separation explicitly:
-
Per-program work queues. The dashboard My Queue panel partitions rows by program. A SNAP worker’s queue contains only SNAP work items; a TANF worker’s only TANF.
-
Per-program intake flows.
/applications/{id}/intake?program={slug}is a per-program page. The TANF flow doesn’t display SNAP data; the SNAP flow doesn’t display TANF data. -
No cross-program data auto-fill. When Maria’s TANF intake opens, the demographic fields are NOT pre-populated from her SNAP intake. The data was re-collected from the same paper form. This is the #602 client-data-isolation principle made visible.
-
Independent determination paths. Each program’s
POST /v1/{program}/determineis independent. The SNAP determination doesn’t gate the TANF determination. -
Separate audit chains. Per-program audit rows. SNAP’s
determination.completed.snapand TANF’sdetermination.completed.tanfare separate events with their own hash-chain entries.
The ONE cross-program affordance is the ELE flag, which is genuinely cross-program (any of 4 source programs grants it). That’s the right thing — ELE is the federal-policy reason these programs CAN talk to each other in a regulated way. It’s the exception that demonstrates the rule.
ELE status model (architecturally honest)
CREATE TABLE ele_consents (
id UUID PRIMARY KEY,
household_id UUID NOT NULL,
child_person_id UUID NOT NULL,
consenting_person_id UUID NOT NULL, -- usually the HoH
consenting_worker_id TEXT NOT NULL, -- the caseworker who witnessed
consent_given_at TIMESTAMPTZ NOT NULL,
revoked_at TIMESTAMPTZ,
UNIQUE (child_person_id, revoked_at) -- one active consent per child
);
CREATE TABLE ele_status (
id UUID PRIMARY KEY,
child_person_id UUID NOT NULL,
granted_at TIMESTAMPTZ NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
granting_program_history JSONB NOT NULL, -- [{program, granted_at, source_determination_id}]
revoked_at TIMESTAMPTZ
);
CREATE INDEX idx_ele_status_active ON ele_status (child_person_id)
WHERE revoked_at IS NULL AND expires_at > NOW();
The subscriber logic (simplified):
async fn handle_determination_completed(event: DeterminationCompleted) {
if event.status != "approved" { return; }
if !ELE_SOURCE_PROGRAMS.contains(&event.program) { return; } // snap, tanf, caps, wic
let children = canopy_persons::find_children_in_household(event.household_id).await;
for child in children {
let consent = ele_consents::find_active(child.person_id).await;
if consent.is_none() { continue; }
// Either grant fresh (if no active ELE) or append to history (if active).
match ele_status::find_active(child.person_id).await {
None => ele_status::grant(child.person_id, event.program, 365_days_from_now()).await,
Some(existing) => ele_status::append_source(existing.id, event.program, event.determination_id).await,
}
}
emit_audit_event("ele.granted_or_extended", ...).await;
}
ELE renewal is a separate scheduled job: at expires_at - 30_days, check whether any of the source programs in granting_program_history is still currently approved for this household. If yes, extend expires_at by another year + emit ele.renewed. If none, do nothing (the flag will lapse naturally at expires_at).
This is architecturally honest: event-driven, audit-trailed, per-child, JWS-anchored back to the source determination. It is NOT an application cascade — Medicaid eligibility for the child still requires a separate POST /v1/medicaid/determine; the ELE flag is just a piece of evidence the Medicaid determination can rely on for income verification.
Data-collection section UX
Each of the ~15 SNAP sections + ~5 TANF sections renders as an accordion in /applications/{id}/intake:
-
Closed-state row: section name + one-line summary ("Maria Lopez (HoH), 1 child") + edit icon + green check (complete) or amber dot (incomplete).
-
Open-state: inline form for the section’s fields, htmx-driven save-on-blur or save-button.
-
Per-section audit event on save (
intake.section_updated, withsection_name+ diff). -
"Complete Data Collection" button at the bottom is gated on all required sections being marked complete.
The accordion pattern mirrors the screenshot the SME provided. It scales naturally to the complex case (35 sections instead of 15) by just rendering more accordions.
Steps
Phase A — Foundation
A1: Staged demo archetype
Files: tools/canopy-seed/src/demo/personas.rs, tools/canopy-seed/src/demo/generate.rs
Add archetype intake-demo-maria-lopez-simple. Composition:
-
Adult: Maria Lopez, ~28y, HoH, no employment captured yet (income capture happens during intake on-camera, not in the seed).
-
Child: Liam Lopez, age 7, Maria’s biological son.
-
Programs requested:
["snap", "tanf"]. -
Applications seeded at
submitted(notdetermined). No income/resources/expenses rows. -
Stable
household_idacrosscargo xtask demo regenerate.
A2: 9-step workflow stepper component
Files: services/canopy-web/templates/_primitives/orchard.html (new macro), services/canopy-web/static/css/canopy-web.css (styles)
A horizontal stepper showing the 9 workflow steps with state per step: complete, current, pending, skipped (for the 4/5/6/8 steps in simple cases). Renders at the top of /applications/{id}/intake and updates as the worker advances.
A3: /applications/{id}/intake page shell
Files: services/canopy-web/src/api/intake.rs (new), services/canopy-web/templates/intake/page.html (new), services/canopy-web/src/api/mod.rs (route)
Page shell with:
-
Page title
Intake — {Program label}viacrate::format::humanize_program. -
Stepper component (A2) at top.
-
Vertical accordion of data-collection sections (Phase B + C content).
-
"Complete Data Collection" button at bottom (advances Step 7).
-
"Run Determination" button after Step 7 (advances Step 9).
Route: GET /applications/{id}/intake?program={slug}.
A4: Per-program work-queue split
Files: services/canopy-web/src/dashboard/panels/my_queue.rs, services/canopy-web/templates/dashboard/panels/my_queue.html
Partition the My Queue panel by program. Each row carries a program field (already does — humanized via crate::format::humanize_program). Group the rendered rows by program in the template; show per-program counts. The dashboard composition surfaces this as a single panel for the demo’s purposes; longer-term this is on the path to #607 (work tracking + assignment revamp).
Phase B — SNAP intake sections (15 sections)
Each section follows the same shape:
-
A struct in
crates/canopy-contracts-applications(or local to canopy-applications) for the section’s payload. -
POST /v1/applications/{id}/sections/{name}endpoint in canopy-applications. -
A
services/canopy-web/src/case_detail/intake_sections/{name}.rsplugin (analogous to the case-detail sections from Stage 5). -
An Askama template for the accordion row + open-state form.
-
A unit test that round-trips the section payload through the endpoint.
B1–B11
The 11 work items (B1–B11) cover the 15-16 simple-case sections from the SME workflow diagram. They are listed in the Status table. Each follows the shape above; details for the more complex ones below:
-
B6 ELE Consent: per-child checkbox; captures consenting-person-id + worker-id + timestamp. Writes to
ele_consentstable from the ELE design above. Two-column layout: child name + consent toggle + revocation date (blank when active). -
B11 Work Number / ABAWD Activity / Work Registration: three sub-cards in one section. Pulls from canopy-snap’s existing ABAWD-tracking endpoint where data exists; allows worker entry where not.
B12: Step 7 — Complete Data Collection
Files: services/canopy-applications/src/api/mod.rs, services/canopy-web/src/api/intake.rs
Server-side validation: every required section for the requested programs has content (required per-section is declared in section metadata). Transitions application state submitted → data_collected. Audit event application.data_collected. UI: stepper advances; "Run Determination" button enables.
B13: Step 9 — Complete Case (SNAP)
Already mostly exists (existing Process Application page’s Approve flow). Wire so the /applications/{id}/intake?program=snap page’s "Run Determination" button routes to the same handler. Determination fires, NOA generates, state transitions data_collected → determined.
Phase C — TANF intake (5 additional sections + reuse)
C1: Same /applications/{id}/intake page; ?program=tanf switches the section list
Files: services/canopy-web/src/api/intake.rs
The section accordion is program-aware. SNAP-specific sections (Work Number / ABAWD / Work Registration) hide; TANF-specific sections (Non-Compliance, Time Limits, PRP, Family Cap) show.
C2–C5: TANF-specific sections
Same pattern as B sections.
-
C2 Non-Compliance: pulls from canopy-tanf’s existing sanction-tracking endpoint. Read-only display of sanction history + cure path + current penalty level.
-
C3 Time Limits: pulls from canopy-tanf’s existing time-limit endpoint. Read-only display of months used / months remaining / lifetime cap status.
-
C4 PRP: Personal Responsibility Plan attestation. Worker captures whether PRP signed + when + by whom.
-
C5 Family Cap: state-specific tracking of children excluded from the assistance unit due to Family Cap rules. For Georgia this is currently inactive but the field is captured.
Phase D — Independence demonstration
D1: Per-program work-queue split visible
Already done in A4 — but verify on-camera the SNAP queue and TANF queue are visibly separate.
Phase E — ELE flag + renewal
E1: ele_consents + ele_status tables
Files: services/canopy-eligibility/migrations/{timestamp}_create_ele_tables.sql, services/canopy-eligibility/src/ele/mod.rs (new)
Two forward migrations creating the tables from the design above. The ele_status.granting_program_history JSONB allows append-on-extend without losing the source-program trail.
E2: ELE subscriber
Files: services/canopy-eligibility/src/ele/subscriber.rs (new), services/canopy-eligibility/src/main.rs
Subscribes to determination.completed.snap, determination.completed.tanf, determination.completed.caps, determination.completed.wic. Logic per the pseudocode above. Emits audit events ele.granted (first grant) or ele.extended (subsequent grant where existing active row).
E3: ELE renewal scheduler
Files: services/canopy-eligibility/src/ele/renewal.rs (new)
Daily scheduled job (wrapped in canopy_db::advisory::run_with_advisory_lock per the project’s leader-election pattern). At expires_at - 30_days, check if any source program in granting_program_history is still currently approved (query program_determinations for status = 'approved' and program IN granting_history). If yes, extend expires_at by 365 days + audit ele.renewed. If no, do nothing — flag lapses naturally.
E4: ELE status visible on case-detail
Files: services/canopy-web/src/api/case_detail.rs, services/canopy-web/templates/case_detail/_identity_hero.html
Identity hero gains a child-ELE badge per child showing active/expired + granting program + expiration date. Worker can click for the full granting_program_history audit.
Files Touched
| File | Change |
|---|---|
|
Add |
|
9-step stepper macro (A2) |
|
Stepper styles + accordion section styles |
|
Intake page handler + section dispatch (A3, C1) |
|
Intake page template + accordion |
|
One file per section (B1–B11, C2–C5) |
|
One template per section |
|
Wire shapes for section payloads |
|
|
|
Schema for per-section persistence (likely one JSONB column on |
|
|
|
Store layer + ELE-specific types (E1, E2) |
|
Event subscriber on 4 program events (E2) |
|
Daily renewal scheduler (E3) |
|
Register ELE subscriber + renewal scheduler |
|
Identity hero gains ELE-status block (E4) |
|
Render ELE badge |
|
Per-program partition (A4) |
|
Render per-program groups |
|
Humanize |
CHANGELOG.adoc |
Entry under |
Verification
-
cargo nextest run --workspace --lib— all section round-trip tests pass; ELE subscriber + renewal tests pass. -
cargo xtask dev reload— new migrations apply cleanly; subscribers register on startup. -
cargo xtask seed --profile demo --reset— staged archetype seeds atsubmittedstate. -
Manual walk-through (per-section): load each of the 15+5 sections; enter data; verify save; verify accordion check mark; verify audit event.
-
Manual walk-through (full flow): SNAP intake start → Step 7 → Step 9 → approved → ELE badge visible. Then TANF intake start → Step 7 → Step 9 → approved → ELE granting_program_history shows both programs.
-
Independence check (recording-level): verify on-camera that SNAP approval does NOT change any TANF section’s state; TANF queue rows unchanged until worker actively works them.
-
Renewal check: integration test that fast-forwards
ele_status.expires_atto within 30 days, runs the renewal scheduler, verifies extension when source program still approved + no-op when not. -
Dry-run recording: F1 + F2 + F3.
Documentation Updates
-
CHANGELOG.adoc — entry under
== Unreleased -
Antora: this plan is the spec; update Status table as steps land
-
ADR candidate: ELE event-driven status-flag pattern (per-child, JWS-anchored, append-on-extend). Same pattern will eventually apply to other federal cross-program affordances (TMA, adjunctive Medicaid, school lunch hand-off).
-
After plan completes: update
docs/modules/ROOT/pages/services.adoc(when #613 lands) with the new ELE events + subscribers + tables
Open follow-ups (not blockers for the video)
-
Complex-case workflow (35 sections, full 9 steps, ~85 min real-world) — Phase B/C cover only the simple-case sections. The remaining 60+ sections are post-video buildout.
-
TANF-specific work-participation section (more granular than the SNAP Work Registration). The simple case doesn’t require it.
-
CAPS + WIC ELE source-program coverage on-camera. The subscriber will handle both, but the video doesn’t exercise them.
-
CHIP ELE handoff. Same pattern as Medicaid; not covered in the video.
-
Real ELE policy hierarchy (
SNAP > TANF > CAPS > WICordering matters when multiple are approved simultaneously). The simple "any approval grants 1 year" implementation here is consistent with SME guidance but the hierarchy isn’t surfaced UX-side yet. -
Worker assignment by program role (#607). The demo shows two workers in two sessions; the actual queue-by-assigned-worker logic is a separate epic.