Back to BlogIndustry Guides

A Tenant Screening Workflow: Applications, Checks, Decisions

Rupak Amin

Founder & Lead Engineer, RAITHub

9 min read

RAITHub ships and tests production software. See QA as a Service or talk to us.

A tenant screening workflow is a state machine: application received, checks running, under review, decided, each step explicit and auditable. Background and credit checks are slow third-party calls, so model each as an async step with its own status. Record every decision and who made it. Build the states and audit trail; the fair-process rules governing the decision are legal, so confirm them with your adviser.

This post is engineering guidance only. What you may ask, which checks you may run, and how a decision must be made and communicated are governed by tenancy, fair-housing and data-protection law that differs by jurisdiction; this is general information, so confirm the rules that apply with your adviser. If you would rather have it built for you, see how RAITHub would build this below.

Why build screening as a state machine?

Because an application passes through stages, some of them slow and external, and you must always be able to say exactly where each one is and why. Treating it as a few boolean flags ("checks done?", "approved?") falls apart the moment a check is pending, a provider times out, or a decision is questioned. A state machine makes every stage explicit, makes illegal transitions impossible, and gives you the audit trail that a disputed decision will need.

ApproachProblemState machine instead
Boolean flagsNo single source of truth for "where is this application?"One current state per application, with allowed transitions
Synchronous checksA slow provider blocks the request or times outChecks as async steps with their own pending/complete/failed states
Decision in someone's headNo record of why an applicant was declinedEvery decision logged with its reason and the user who made it
Ad-hoc retriesA re-run double-charges a provider or duplicates a checkIdempotent steps keyed to the application and check

What does the data model look like?

An application has a current state; each external check is its own row with its own status, so one slow or failed check does not block the others. Decisions are recorded, not inferred.

CREATE TABLE applications (
  id         uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  unit_id    bigint NOT NULL REFERENCES units(id),
  applicant_id bigint NOT NULL REFERENCES applicants(id),
  state      text NOT NULL DEFAULT 'submitted',
             -- submitted | checks_running | under_review | approved | declined | withdrawn
  created_at timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE screening_checks (
  id             bigserial PRIMARY KEY,
  application_id uuid NOT NULL REFERENCES applications(id),
  kind           text NOT NULL,                 -- identity | credit | reference | right_to_rent (per local law)
  status         text NOT NULL DEFAULT 'pending', -- pending | complete | failed
  provider_ref   text,
  result_summary text,                          -- a summary, minimising what you store
  created_at     timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX screening_checks_by_app ON screening_checks (application_id, kind);

Store the minimum you need from each check (a pass/fail or a short summary), not the full third-party report, and set a retention rule, because applicant data is sensitive and over-retention is a liability. Which checks are lawful, and what you may keep, is jurisdiction-specific; confirm with your adviser.

How do you handle the external checks?

As async steps, never inline in the request. When an application moves to checks_running, enqueue a job per check that calls the provider, stores the result against that screening_checks row, and advances the application only once every required check is complete. Make each step idempotent, keyed to the application and check kind, so a retry after a timeout does not run the check (or charge the provider) twice. A provider failure sets that check to failed and flags the application for a human, rather than silently approving or declining. This is the same idempotent, recoverable job discipline used across RAITHub's platforms.

Who decides, and how is the decision recorded?

A human, on a defined set of roles, with the reason written down. Model roles with permissions so only authorised staff can approve or decline, and record each decision as an event: the outcome, the reason, the user and the time. This matters for two reasons. First, operations: you can show an applicant and a manager exactly what happened. Second, and more important, the decision is legally sensitive, an applicant may be entitled to know why they were declined and to challenge it, so the record has to exist. Use the role and audit patterns in designing SaaS authorization and an append-only audit log.

How do you test a screening workflow?

Test the transitions and that no check blocks another, plus that every decision is recorded.

import { describe, it, expect } from 'vitest'
import { advance, decide, stateOf } from './screening'

describe('tenant screening workflow', () => {
  it('moves to under_review only when all required checks complete', async () => {
    const app = await seedApplication({ required: ['identity', 'credit'] })
    await completeCheck(app.id, 'identity')
    expect(await stateOf(app.id)).toBe('checks_running')  // credit still pending
    await completeCheck(app.id, 'credit')
    await advance(app.id)
    expect(await stateOf(app.id)).toBe('under_review')
  })

  it('records the reason and the decider on a decline', async () => {
    const app = await seedUnderReview()
    await decide(app.id, { outcome: 'declined', reason: 'incomplete references', byUser: 7 })
    const log = await decisionLog(app.id)
    expect(log.at(-1)).toMatchObject({ outcome: 'declined', byUser: 7 })
  })
})

Add a test that a failed provider check flags the application for a human rather than auto-deciding, and one that a retried check does not run twice.

Buy, build or hire?

OptionChoose this whenThe catch
A screening provider's hosted flowTheir checks, market and decision model fit your processYou fit their workflow and data model; your own states, roles and local rules may not map
Email and a spreadsheetA few applications and one person decidingNo audit trail, no consistent process, and nothing to show if a decision is challenged
Custom workflow, provider checks plugged inYou screen at volume, need your own process and audit trail, or integrate several providersYou own the workflow and the record; the checks themselves still come from licensed providers

How long does it take to build yourself, and what is the risk?

For an experienced developer building the application-to-decision workflow with async provider checks, roles and an audit trail, our estimate is 4 to 6 weeks at a fixed scope. The main risk is not technical: tenant screening is heavily regulated. Fair-housing and anti-discrimination rules, what you may ask and check, consent, and an applicant's right to an explanation differ by jurisdiction, and getting them wrong is a legal exposure, not just a bug. This is general information, so build the workflow with your adviser defining the rules it must enforce.

Why RAITHub for this

  • Workflows and roles in production. RAITHub built PropDesk with four roles and five daily automation jobs (1,024 tests), and Sundor Skin with 12 staff roles from 88 permission codes and a hash-chained audit log (530+ tests).
  • Audit trails by default. Every sensitive action is recorded, which is exactly what a disputed screening decision needs.
  • Honest limits. RAITHub builds the workflow and the audit trail; it does not give legal advice on what you may screen for, and will say so on the first call.

When you don't need us

  • A provider's hosted flow fits. If its checks and decision model match your market and process, use it.
  • Volume is tiny. A documented manual process may be enough for a few applications.
  • You only need the model. The state machine and audit pattern above are a fair start for your own developer, with your adviser setting the rules.

How RAITHub would build this

  • Workflow: an application-to-decision state machine with allowed transitions and no illegal jumps.
  • Async checks: each provider check as its own idempotent step with pending/complete/failed states; failures flagged for a human.
  • Roles and audit: permission-based approval, every decision recorded with reason, user and time, and data minimised and retained to a set rule.
  • Tests: transition, blocking, idempotency and decision-record suites gated in CI.

Timeline: a first version fits the 4 to 6 week fixed-scope range; multiple providers and reporting push it toward the 6 to 12 week backend range. See SaaS development and the real-estate industry page. A related build is a rent arrears and collections system.

You receive: the workflow, provider integration and audit code, tests gated in CI, handover docs, and full IP in your name under NDA. Legal rules are defined by your adviser, not claimed by RAITHub.

Next step: book the free 15-minute technical audit with your screening steps and the rules your adviser has set, and we will follow up with a written fixed quote.

Frequently asked questions

Why build tenant screening as a state machine?

Because an application moves through stages, some slow and external, and you must always know exactly where each one is and why. A state machine makes every stage explicit, blocks illegal transitions, and gives you the audit trail a disputed decision needs, which boolean flags cannot.

How should background and credit checks be handled?

As async steps, not inline in a request. Enqueue a job per check that calls the provider and stores the result against its own row, advancing the application only when every required check completes. Make each step idempotent so a retry after a timeout does not run or charge for the check twice.

Who should be able to approve or decline an applicant?

Only authorised staff, controlled by permission-based roles, with the reason written down. Each decision is recorded as an event with the outcome, reason, user and time, because the decision is legally sensitive and an applicant may be entitled to know why they were declined.

What applicant data should the system store?

The minimum needed: a pass/fail or short summary of each check, not the full third-party report, with a set retention rule. Applicant data is sensitive, and over-retention is a liability. What you may collect and keep is jurisdiction-specific, so confirm it with your adviser.

Does RAITHub advise on what we can screen for?

No. RAITHub builds the workflow, the provider integrations and the audit trail. Which checks are lawful, what you may ask, and how a decision must be made and communicated are legal matters that differ by jurisdiction; this is general information, so your adviser defines the rules the system enforces.

Can I plug in my own screening provider?

Yes. Each check is modelled as its own async step, so an identity, credit or reference provider is integrated as a plug-in with its own status, and you can run several. The workflow and the record stay yours; the checks themselves come from the licensed providers you choose.

tenant screeningworkflowstate machineproperty managementaudit trailproptech

Ready to discuss your project?

Book a free 15-minute technical audit with our engineering team.