Appearance
Clinician credentialing
Credentialing a clinician before their first shift, decided by rules, an AI model and a credentialing committee together: one flow collects the license, its status and expiration, the malpractice history and the clinician's own account of investigations, conditions and gaps in practice; the clinician is checked against the exclusion list you keep, a model reads their account before a clean file is credentialed, and files with findings go to the committee with the model's brief. Every rule result, model answer and decision is on the run's record. Pack: examples/policies/clinician-credentialing/.
A pattern for expressing a healthcare credentialing policy, not compliance guidance: the thresholds, lists, fields and questions are placeholders chosen to make the example run; a real policy comes from the business's own rules. Its model steps call one install, chatgpt (The chatgpt install); the exclusion check is the list's, never the model's.
The decision it automates
Whether to credential the clinician, refer the file to the committee, or decline it, when the file is submitted. The rules decide; the model can only send a file to the committee, never credential or decline a clinician.
Data expected
- The clinician as a subject (kind
user); nothing is read from its data. - The flow's answers under
submission:fullName,licenseNumber,licenseStatus,licenseExpiry(YYYY-MM-DD),malpracticeClaims,practiceHistory(investigations, complaints, conditions on the license or gaps in practice in the last five years, in the clinician's words, “None” when there are none; at most 2,000 characters) and an optional copy of the license. - The
credentialing_exclusionslist, which the pack creates with a placeholder row. - An install named
chatgptof the platform'schatgptapp, with your OpenAI API key.
Steps and rules
| Step | Type | Why |
|---|---|---|
collect | wait_for_collection | The credentialing flow. |
rules | evaluate_rules | The credentialing rule set under max_severity. |
route | branch | rules.outcome == reject (an exclusion, an expired license) goes straight to the decision, without the model; any other file to read. |
read | call_app | decide through chatgpt on practiceHistory: one question, answered under ai. |
gate | branch | rules.outcome == manual_review goes to brief; ai.answers.clear.probability >= 0.8 credentials a file the rules approve; the rest go to brief. |
brief | call_app | generate through chatgpt: the committee's brief, under brief. |
review | create_case | credentialing_committee, 7-day SLA. |
decide | emit_decision | From the rules, or the committee's decision. |
The rule set credentialing (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 |
|---|---|---|---|
cred_not_excluded | list lookup | block, 100 | fullName is on credentialing_exclusions |
cred_license_active | comparison | block, 100 | licenseStatus is expired |
cred_license_restricted | comparison | warn, 30 | licenseStatus is restricted |
cred_expiry_soon | expression | warn, 20 | licenseExpiry is less than 90 days away |
cred_malpractice | score threshold | warn, 30 | malpracticeClaims is above 1 |
The model steps
read gives the model the clinician's statement, practiceHistory, in a text the step writes (its stateTemplate), and nothing else of the file, and asks one question:
| Question | Answer | Asks | Read by |
|---|---|---|---|
clear | yes or no, threshold 0.8 | Does the statement say there is nothing to disclose: no investigation, complaint, claim, condition on the license or gap in practice of more than six months? A change of employer without a break counts as nothing. | gate: a file the rules approve is credentialed only on a yes at 0.8 or more |
A probability under 0.8, a model that cannot tell included, sends the file to the committee: a statement that mentions what the structured answers do not show is the committee's to weigh (Routing on an answer). The question's threshold is the branch's, so ai.answers.clear.value says which way a clean file went.
brief runs only on the way to the case, from the license status and expiration, the malpractice count and the statement, and answers a CommitteeBrief object: summary, matters (each with what, date and status, null where the statement does not say) and questions, what the committee may want to ask or see. Its instructions keep it from saying whether to credential the clinician or whether they appear on any exclusion, sanctions or licensing list, and from guessing why a gap in practice happened.
What the vendor sees
The statement, and for the brief the license status, its expiration date and the malpractice count, through the install's connection: not the name, the license number or the copy of the license; no document or image reaches a model (Data protection). A statement may describe investigations and their outcomes, so choose the install's vendor and its retention terms with that in mind; the form asks for the dates of a gap in practice, not why it happened. With the one-click install's configuration, both steps reach OpenAI's Responses API, with storing off. The clinician writes the statement: at worst, an answer the model was talked into credentials a file every rule passed, 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, so every file the rules do not decline fails at read, 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 clinician 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 credentialing_committee case with the file, the copy of the license, each rule's result, the model's calls under Vendor checks, and its answer (ai) and brief (brief) in the run context; the committee's notes and attachments become part of the record.
How to adapt it
Load the exclusion lists you check into credentialing_exclusions, or check through your sources' APIs, each through an http-api install (Adding a vendor step): a call_app step between collect and rules for the exclusion check, with cred_not_excluded reading its output, and a license lookup so status and expiration come from the source rather than the form. The model answers neither. Write the question and the brief for your committee, and move the 0.8 in gate together with the question's threshold. Your scheduler starts the same workflow again before each credential expires.
Run it
bash
pnpm policy:import examples/policies/clinician-credentialing --publishThe check runs two fixtures against the model stub (scripts/ai-stub.mjs), whose answers a cue word in the statement picks. expect.json submits an active license with a restriction that expires in 2030, no malpractice claims and a statement describing a supervision condition: the clinician is not on the exclusion list and the restriction rule fails, so the file goes to the committee whatever the model says. Cued low, the stub answers clear no at 0.05 and writes a brief; a credentialing_committee case opens, the check approves it and the run completes with a manual approve. expect-credentialed.json submits an active license with no findings and a statement that names only a change of employer: every rule passes, the stub answers clear yes at 0.95, and the run completes with an automated approve, no case opened.