Skip to content

Grant eligibility ​

A household applies for a grant, and rules, an AI model and a caseworker decide together: an accessible flow takes the region, the household, its income and any change in its circumstances, with evidence; eligibility rules written from the program's text decide; a model reads the applicant's account of changes before any automatic award or denial; and near-limit, incomplete or changed applications go to a caseworker with the model's brief. Every rule result, model answer and decision is on the run's record. Pack: examples/policies/grant-eligibility/.

A pattern for expressing a public sector & grants policy, not compliance guidance: the thresholds, lists, fields and questions are placeholders chosen to make the example run; a real policy comes from the program's own rules. Its model steps call one install, chatgpt (The chatgpt install).

The decision it automates ​

Whether the household is eligible, needs a caseworker, or is not eligible, when the application is submitted. The rules decide eligibility; the model can only send an application to a caseworker, so an award or a denial is automatic only when the applicant describes no change that could put the figures out of date.

Data expected ​

  • The applicant as a subject (kind user); nothing is read from its data.
  • The flow's answers under submission: region, householdSize, income (USD a year), circumstances (any change in the last 12 months or expected soon to work, income, where the household lives or who lives in it, in the applicant's words, “None” when there is none; at most 2,000 characters) and incomeEvidence (files).
  • An install named chatgpt of the platform's chatgpt app, with your OpenAI API key.

Steps and rules ​

StepTypeWhy
collectwait_for_collectionThe grant-application flow.
rulesevaluate_rulesThe grant-eligibility rule set under max_severity: residency and the income limit are block, the rest warn.
readcall_appdecide through chatgpt on the figures and circumstances: one question, answered under ai.
gatebranchrules.outcome == manual_review goes to brief; ai.answers.unchanged.probability >= 0.8 lets the rules' award or denial stand; the rest go to brief.
briefcall_appgenerate through chatgpt: the caseworker's brief, under brief.
reviewcreate_casegrant_caseworker, 10-day SLA.
decideemit_decisionFrom the rules, or the caseworker's decision.

The rule set grant-eligibility (useCase: custom) uses max_severity: a failed block rule rejects, a failed warn rule sends the run to review, and the weights add up to the score.

RuleTypeSeverity, weightFails when
grant_residencycomparisonblock, 100region is not North, South or East
grant_income_limitexpressionblock, 100income is above $15,000 + $7,500 × householdSize
grant_near_limitexpressionwarn, 20income is above 90% of that limit
grant_evidencecomparisonwarn, 30no evidence of income was uploaded

grant_evidence reads submission.incomeEvidence.0.fileId: the field takes several files, which the answers hold as a list, so the rule passes when the first one is there.

The model steps ​

read writes the model the household's figures, householdSize, region and income, and the applicant's account of changes, circumstances (its stateTemplate, filled through variables; not the applicant's name), and asks one question:

QuestionAnswerAsksRead by
unchangedyes or no, threshold 0.8Do the applicant's own words suggest the figures they gave still describe the household: the same people, the same region and income from the same work?gate: the rules' award or denial stands only on a yes at 0.8 or more

The step runs for every application, those the rules turn down included: a denial on figures the applicant says have changed, or one the model cannot confirm, goes to a caseworker instead. The model never weighs eligibility; the program's rules and the caseworker do (Routing on an answer). The question's threshold is the branch's, so ai.answers.unchanged.value says which way an application the rules decided went.

brief runs only on the way to the case, from the same four answers, and answers a CaseworkerBrief object: summary, changes (each with what, date, null when the applicant does not say, and effect: income_lower, income_higher, household_changed, moved or other) and evidence, what the caseworker may ask the applicant for. Its instructions keep it from saying whether the household is eligible or what it should receive, and from guessing why a change happened.

What the vendor sees ​

The figures, the account of changes and the steps' own text, through the install's connection: not the applicant's name or the evidence files; no document or image reaches a model (Data protection). An account may touch on work or family circumstances, so choose the install's vendor and its retention terms with that in mind; the form asks what changed and when, not why. With the one-click install's configuration, both steps reach OpenAI's Responses API, with storing off. The applicant writes the account: at worst, an answer the model was talked into lets the rules' own award or denial stand, as the rules alone would (Prompt injection).

The chatgpt install ​

A pack never carries an install: its steps call one by name, so the pack declares it, "installs": { "chatgpt": "chatgpt" }, the platform's ChatGPT (OpenAI API). The import reports chatgpt missing (--publish publishes all the same), and the admin console's import result installs it in one click, with a preset configuration and a sample key in place of yours. Store your OpenAI API key on the install's page (Connect › Apps, the install chatgpt, Configuration › Keys, openaiApiKey) before the workflow runs.

Until you do, the run fails: OpenAI refuses the sample key, and every application reaches read, so every run fails there, with no case and no decision. A model call that fails once the key is in does the same: an outage that outlasts the retries, an exhausted quota or a refusal fails the run at read or brief. The vendor's answer is on the call's invocation record, under the run's app calls; once the call can succeed, replay the run from read (Operate › Runs, Replay from step), and the applicant is not asked again. Without any install named chatgpt, the run fails at read with unknown_install. To ask Claude or Gemini instead, install that app under a name of your own and name it in the steps' app and in the pack's installs: every model app answers decide and generate in the same shape (Installing a model app).

What a reviewer sees ​

A grant_caseworker case with the answers, the evidence, each rule's result, the model's calls under Vendor checks, and its answer (ai) and brief (brief) in the run context, so the award or denial letter can quote the rule that applied. Request info asks the applicant for what the brief lists, such as pay stubs since a change of job; when they send the form, the rules and the model run again on the new answers (The case page).

How to adapt it ​

Write your program's formula as the CEL expressions, add steps for each household member, look applicants up in your agency's records through an http-api install for each system's API, never a model (Adding a vendor step), and add an appeal workflow that a different caseworker reviews. Write the question and the brief for your program, and move the 0.8 in gate together with the question's threshold.

Run it ​

bash
pnpm policy:import examples/policies/grant-eligibility --publish

The check runs two fixtures against the model stub (scripts/ai-stub.mjs), whose answers a cue word in the account of changes picks. expect.json is a household of three earning $34,000 with no evidence, whose earner went down to three days a week in July: under the limit of $37,500 but within 10% of it, so the near-limit and evidence rules fail and the application goes to a caseworker whatever the model says. Cued low, the stub answers unchanged no at 0.05 and writes a brief; a grant_caseworker case opens, the check approves it and the run completes with a manual approve. expect-not-eligible.json is a household of two in the West, outside the program's regions, with nothing changed: the residency rule rejects, the stub answers unchanged yes at 0.95, and the run completes with an automated reject, no case opened.

Released under the Apache-2.0 License.