Appearance
Tenant screening
Screening an applicant for a rental home, decided by rules, a model and a person together: the applicant answers one flow with the rent, their income with an optional note about it, their employer or tax return and two references; the same four rules apply to everyone; when the applicant writes a note and the rules do not decline, a model reads it, weighing only what it says about money; borderline applicants and notes the model is not sure of go to the leasing team. Every rule result, model answer and decision is on the run's record. Pack: examples/policies/tenant-screening/.
A pattern for applying the same criteria to every applicant, not legal advice: the thresholds, questions and fields are placeholders chosen to make the example run; a real policy comes from the business's own rules, and which criteria a landlord may apply is a question for the landlord's counsel, never for a model. Its model step calls one install, chatgpt (The chatgpt install).
The decision it automates
Whether to accept the applicant, refer them to the leasing team, or decline, when the application is submitted. Only the rules accept or decline on their own: the model can send an applicant the rules accept to the leasing team, never accept or decline anyone.
Data expected
- The applicant as a subject (kind
user) withidentityVerified, written by your platform from its identity check. - The flow's answers under
submission:rentandmonthlyIncome(USD, income before taxes), the optionalincomeNote(1,000 characters at most),employment, thenemployerNameand an optionalpayStub, or an optionaltaxReturnfor the self-employed, andreference1Emailandreference2Email. - An install named
chatgptof the platform'schatgptapp, with your OpenAI API key.
Steps and rules
| Step | Type | Why |
|---|---|---|
collect | wait_for_collection | The tenant-application flow; self-employed applicants are asked for a tax return instead of an employer. |
rules | evaluate_rules | The tenant-screening rule set under max_severity. |
gate | branch | rules.outcome == reject goes straight to the decision; an application with an incomeNote goes to income_check, one without to route, with no model asked. |
income_check | call_app | decide through chatgpt on the note: two questions, answered under incomeCheck. |
route | branch | rules.outcome == manual_review opens a case, and so does incomeCheck.answers.income_current.probability under 0.8; the rest go to the decision. |
review | create_case | leasing_review, 48-hour SLA. |
decide | emit_decision | From the rules, or the reviewer's decision. |
The rule set tenant-screening (useCase: onboarding) 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.
| Rule | Type | Severity, weight | Fails when |
|---|---|---|---|
tenant_identity_verified | comparison | block, 100 | subject.identityVerified is not true |
tenant_rent_ceiling | expression | block, 100 | rent is above 60% of monthlyIncome |
tenant_rent_to_income | expression | warn, 40 | rent is above 40% of monthlyIncome |
tenant_two_references | comparison | warn, 20 | no second reference was given |
The model step
income_check runs only when the applicant wrote a note and the rules did not decline. Its state (stateTemplate) is one sentence and the note, with a literal default should the note be gone, and it asks two questions:
| Question | Answer | Asks | Read by |
|---|---|---|---|
income_current | yes or no, threshold 0.8 | By the note, is the income the applicant gave already being paid to them every month? Wages, self-employment income, a pension, Social Security or other benefits, child support, parental leave pay and any other regular payment count, whatever their source; a job that has not started, a raise still to come or a one-time payment do not. | route: under 0.8 opens a case |
support | yes or no | Does the note say a guarantor or co-signer, savings set aside for the rent or a co-applicant's income will help pay the rent? | The leasing team, in the run context |
Both questions tell the model to weigh only what the note says about money, never anything about the applicant as a person: not their race, color, national origin, religion, sex, family or pregnancy, disability, health or age. The form's help asks the applicant to write only about money, too. The model's answer can only add a person's look: an applicant the rules accept and whose note the model is not sure of goes to the leasing team, who decide on the same criteria as for everyone else. Which criteria a landlord may apply, and which income it must count, is a question for the landlord's counsel, not for a model.
What the vendor sees
Through the install, OpenAI receives the step's sentence, the applicant's note and the two questions. Nothing else reaches it: not the applicant's name, the rent or the income figures, the employer, the references, or the pay stub and tax return; no document or image reaches a model (Data protection). An application without a note, or one the rules decline, is never sent. The note is personal data the applicant wrote; the one-click install calls OpenAI's Responses API with storing off.
The applicant may write instructions into the note: at worst, an answer the model was talked into lets through an applicant the rules accepted, 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 (an OpenAI connection through the Responses API, the console's first suggested model, OpenAI's hosts approved) and a sample key in place of yours. Store your OpenAI API key on the install's page (Configuration › Keys › openaiApiKey) before the workflow runs. Through the API, create the install as Installing a model app shows, named chatgpt.
Until you do, the run fails: OpenAI refuses the sample key, so every application with a note that the rules do not decline fails at income_check, with no case and no decision; applications without a note are decided by the rules alone. 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 income_check, and so does a workspace without any install named chatgpt. The vendor's answer shows in the invocation record and under the run's app calls; once the call can succeed, replay the run from income_check (Operate › Runs, Replay from step), and the applicant is not asked again. To ask Claude or Gemini instead, install that app under a name of your own and name it in the step's app and in the pack's installs: every model app answers decide in the same shape.
What a reviewer sees
A leasing_review case with the answers, each criterion met or missed, and the model's answers in the run context (incomeCheck: whether the income is already being paid, with its probability, and whether the note offers support), so the landlord can be told why. The model's call is under Vendor checks.
How to adapt it
Ask the model what your rental policy weighs about income, keep the questions about money, and let a doubt go to the leasing team rather than to a decline. Take the rent from your listing instead of the form, and add your tenant screening provider through an http-api install with a webhook, as a start step whose run resumes when the provider's webhook says the reports came back (Waiting for the vendor), and a co-signer step for applicants above the threshold. A provider that answers at once, such as an income check, takes a request step instead (Adding a vendor step).
Run it
bash
pnpm policy:import examples/policies/tenant-screening --publishThe check runs two fixtures against the model stub (scripts/ai-stub.mjs), whose answers a cue word in the applicant's note picks. expect.json applies for a rent of $1,850 on an income of $4,200 (44%), with a note that a co-signer will sign the lease: the rent-to-income rule fails, so the application goes to the leasing team whatever the model says; cued high, the stub answers both questions yes at 0.95, a leasing_review case opens, the check approves it and the run completes with a manual approve. expect-approved.json applies for $1,400 on $5,200 (27%), with a note about six years at a hospital in Columbus and savings set aside: every rule passes, the stub answers income_current yes at 0.95, and the run completes with an automated approve, no case opened.