TutorialEvaluation & observability
How to test prompts and LLM outputs with promptfoo, with a CI step
Set up LLM evaluation with promptfoo: write a YAML test suite with assertions, compare two models side by side and fail a CI build on regressions.
LLM evaluation with promptfoo means writing your prompts, the models to call and a set of test cases in one YAML file, then running promptfoo eval. Each test case carries assertions, such as "output is valid JSON" or "a grader model agrees the answer follows this rubric". The command exits with a non-zero code when tests fail, so the same suite can block a pull request in CI.
promptfoo (opens in a new tab) is an open-source CLI and library under the MIT licence. In March 2026 promptfoo announced it was joining OpenAI (opens in a new tab), and the README now says it is part of OpenAI. The announcement says the open-source tool will stay maintained and keep supporting other providers and models.
Why prompts need regression tests
A prompt is code that a model interprets. Small edits to the wording, a new model version or a different temperature can change outputs on inputs you never looked at. Manual spot checks catch the cases you thought of today and miss the ones you fixed last month.
A regression suite turns those past cases into checks. Every time you edit a prompt or switch models, the suite runs the same inputs and reports which ones broke. promptfoo's CI documentation lists catching regressions before they reach production as the first reason to run evals in a pipeline.
Installing promptfoo
The npm package needs Node.js 22.22.0 or newer, and the docs recommend Node.js 24 LTS. Install it globally, through Homebrew, or run it without installing:
npm install -g promptfoo
# or
brew install promptfoo
# or, with no install
npx promptfoo@latest init --example getting-started
The project README also lists pip install promptfoo. Set the API key for each provider you plan to call:
export OPENAI_API_KEY=...
export ANTHROPIC_API_KEY=...
promptfoo init starts an interactive setup. promptfoo eval setup opens a browser-based version of the same flow.
A first test suite: prompts, providers and test cases
A promptfoo config has three main parts. prompts holds the templates, with variables in double curly braces. providers lists the models. tests lists the inputs and what each output must satisfy.
The suite below checks a support-ticket triage prompt. The tickets and team names are illustrative sample data.
# prompts/triage.txt
Classify this support ticket. Reply with JSON only, in the form
{"team": "billing" | "technical" | "account", "priority": "low" | "high"}.
Ticket: {{ticket}}
# promptfooconfig.yaml
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
description: Ticket triage regression suite
prompts:
- file://prompts/triage.txt
providers:
- id: openai:gpt-6-sol
label: gpt-6-sol
- id: anthropic:messages:claude-sonnet-5-5
label: claude-sonnet-5-5
defaultTest:
assert:
- type: is-json
value:
type: object
required: [team, priority]
tests:
- description: Double charge goes to billing
vars:
ticket: I was charged twice for my March invoice.
assert:
- type: javascript
value: JSON.parse(output).team === 'billing'
- description: Outage is high priority
vars:
ticket: Your API has returned 500 errors for every request since 9am.
assert:
- type: javascript
value: JSON.parse(output).priority === 'high'
- description: Password reset goes to account
vars:
ticket: I can't log in and the reset email never arrives.
assert:
- type: javascript
value: JSON.parse(output).team === 'account'
- type: not-icontains
value: sorry
- description: Reply contains no prose around the JSON
vars:
ticket: How do I export my data as CSV?
assert:
- type: llm-rubric
value: The output is a single JSON object with no explanation before or after it.
defaultTest applies its assertions to every test, so each case also has to pass the JSON schema check. Tests can live in separate YAML, JSONL, CSV or JavaScript files and load with tests: file://tests/*.
Run the suite and open the results:
promptfoo eval
promptfoo view
promptfoo eval runs every prompt against every provider and every test case. promptfoo view starts a local web UI with the results grid.
Which promptfoo assertion types should you use?
The promptfoo assertions reference (opens in a new tab) splits assertions into deterministic checks, which run code against the output, and model-assisted checks, which call a model. Any assertion can be inverted by adding not- to its type.
| Need | Assertion type | Cost to run | Notes from the docs |
|---|---|---|---|
| Exact or partial text | equals, contains, icontains, contains-any, contains-all, starts-with, regex | Free | Deterministic |
| Structured output | is-json (optional JSON schema), contains-json, is-sql, is-xml | Free | Use JSON.parse(output) in javascript to check fields |
| Custom logic | javascript, python, ruby, webhook | Free | Return true/false or a score; pair a score with threshold |
| Speed and spend | latency (milliseconds), cost (dollars) | Free | latency needs the cache disabled with --no-cache; cost needs the provider to report cost |
| Closeness to a reference | similar, levenshtein, rouge-n, bleu | similar calls an embedding model | similar uses embeddings and cosine similarity with a threshold |
| Subjective quality | llm-rubric, g-eval, factuality, model-graded-closedqa | One grader call per check | Grader model depends on which API keys are set unless you override it |
| RAG answers | context-faithfulness, context-recall, context-relevance, answer-relevance | Grader calls | Default threshold 0.5 |
| Refusals | is-refusal | Free | Useful as not-is-refusal for prompts that should be answered |
Each assertion has a weight (default 1). A test case can also set its own threshold: the test passes when the weighted average score reaches it. Without one, every assertion must pass.
The grader for llm-rubric and other model-graded checks can be pinned. Pass --grader <provider> on the command line, set provider on the assertion, or set defaultTest.options.provider for the whole suite. Pinning matters in CI, because otherwise the grader is chosen from whichever API keys the environment has.
Comparing two models side by side
Listing two providers, as the suite above does, is the comparison. promptfoo runs each test against both, and the web viewer puts the two outputs for each test next to each other with their pass or fail result. The same works for prompts: list two prompt files and every test runs against both versions.
To try different models without editing the config, override providers on the command line:
promptfoo eval -r openai:gpt-6-sol openai:gpt-6-luna
A few flags help with a fair comparison:
--repeat <n>runs each test several times, which shows how often a model fails rather than whether it failed once.--no-cacheforces fresh calls. promptfoo caches provider responses on disk (~/.promptfoo/cacheby default), which saves money but hides run-to-run variation.--filter-providers <regex>reruns only one model.-o results.json(or.html,.csv,.junit.xml) writes the results to a file.
The OpenAI provider docs suggest using a dated model snapshot when one exists to keep the version constant across runs, and note that a fixed snapshot still does not guarantee identical outputs.
Running LLM evaluation with promptfoo in CI
promptfoo eval returns exit code 100 when at least one test fails, and exit code 1 for other errors. Any non-zero code fails a CI step. Two environment variables tune that behavior: PROMPTFOO_PASS_RATE_THRESHOLD sets a minimum pass rate as a percentage (default 100, meaning no failures allowed), and PROMPTFOO_FAILED_TEST_EXIT_CODE changes the exit code for failed tests.
Here is a GitHub Actions workflow built from the commands in promptfoo's CI/CD guide (opens in a new tab):
# .github/workflows/prompt-tests.yml
name: Prompt regression tests
on:
pull_request:
paths:
- 'prompts/**'
- 'promptfooconfig.yaml'
jobs:
eval:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '24'
- name: Cache promptfoo
uses: actions/cache@v4
with:
path: ~/.cache/promptfoo
key: ${{ runner.os }}-promptfoo-${{ hashFiles('prompts/**') }}
restore-keys: |
${{ runner.os }}-promptfoo-
- name: Run eval
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
PROMPTFOO_CACHE_PATH: ~/.cache/promptfoo
PROMPTFOO_PASS_RATE_THRESHOLD: '95'
run: >
npx promptfoo@latest eval
-c promptfooconfig.yaml
--grader openai:gpt-6-sol
-o results.json -o results.junit.xml
- name: Upload results
if: always()
uses: actions/upload-artifact@v4
with:
name: promptfoo-results
path: |
results.json
results.junit.xml
The JUnit file plugs into CI test-report viewers. --tag git.sha=... attaches pipeline context to the saved eval. For a before-and-after comment on each pull request, promptfoo publishes promptfoo-action (opens in a new tab), which takes the prompt globs, the config path and a GitHub token.
Leave --share out of CI runs that touch real customer data. That flag uploads results to create a shareable URL.
A pre-merge checklist for a prompt suite
- Every bug report about a bad output becomes a test case before the fix.
- Each test has at least one deterministic assertion, so a grader outage cannot pass it.
- The grader model is pinned with
--graderordefaultTest.options.provider. - Latency checks run in a separate job with
--no-cache. - The pass-rate threshold is written down and changed only on purpose.
- Results are uploaded as an artifact so failures can be inspected without rerunning.
What automated evals cannot judge
Assertions check what you thought to check. A suite of 40 passing tests says nothing about input number 41, so coverage depends on how well your cases reflect real traffic.
Model-graded assertions are themselves model outputs. An llm-rubric grader can be wrong, can drift when its provider updates the model, and costs a call per check. promptfoo's docs note that without a threshold, a model-graded check passes on the grader's pass field alone, so {"pass": true, "score": 0} counts as a pass.
Some properties need people. Tone for your brand, whether advice is safe for a specific user, and whether an answer is true for facts the grader does not know all sit outside what an automated check can confirm. promptfoo's getting-started guide says asserts are optional and that many people get value from reviewing outputs manually in the web viewer.
The docs also state that cost and latency assertions check completed responses. They do not cap spending or stop slow requests, so production limits still belong in your application.
Frequently asked questions
Do promptfoo evals send my prompts to promptfoo's servers?
`promptfoo eval` calls the model providers you configure and writes results to a local promptfoo directory. Sharing with `--share` or `promptfoo share` uploads results to create a URL. promptfoo collects basic usage telemetry by default, which its docs say excludes prompts, outputs and test cases; set `PROMPTFOO_DISABLE_TELEMETRY=1` to turn it off.
Can I test my own application instead of a raw model?
Yes. Besides hosted models, promptfoo providers can be custom Python or JavaScript code and local models through Ollama, so a provider can call your own function or API. The assertions then check whatever your application returns.
How many test cases should a first suite have?
promptfoo sets no minimum. A practical start is the handful of core use cases plus every failure you have already seen in production, as the getting-started guide suggests thinking about core use cases and likely failures. Add a case each time a new bug appears.
How do I keep model-graded tests from making CI expensive?
Cache provider responses between runs with `actions/cache` and `PROMPTFOO_CACHE_PATH`, so unchanged prompts reuse earlier outputs. Use deterministic assertions wherever a code check can decide, and reserve `llm-rubric` for the cases that need judgment. `--filter-first-n` and `--filter-sample` run a subset when you only need a quick signal.
Sources
- 1.GitHub - promptfoo/promptfoo: Test your prompts, agents, and RAGs. Red teaming/pentesting/vulnerability scanning for AI. Compare performance of GPT, Claude, Gemini, DeepSeek, and more. Simple declaratRepositoryPrimarygithub.com
- 2.Getting started | PromptfooOfficial docsPrimarypromptfoo.dev
- 3.Assertions and Metrics - LLM Output Validation | PromptfooOfficial docsPrimarypromptfoo.dev
- 4.promptfoo.dev/docs/installationWeb
- 5.github.com/promptfoo/promptfoo/blob/main/site/docs/providers/openai.mdRepositoryPrimary
- 6.github.com/promptfoo/promptfoo/blob/main/site/docs/providers/anthropic.mdRepositoryPrimary
- 7.github.com/promptfoo/promptfoo/blob/main/site/docs/configuration/expected-outputs/deterministic.mdRepositoryPrimary
- 8.github.com/promptfoo/promptfoo/blob/main/site/docs/configuration/expected-outputs/javascript.mdRepositoryPrimary
- 9.github.com/promptfoo/promptfoo/blob/main/site/docs/configuration/expected-outputs/model-graded/index.mdRepositoryPrimary
- 10.github.com/promptfoo/promptfoo/blob/main/site/docs/configuration/expected-outputs/model-graded/llm-rubric.mdRepositoryPrimary
- 11.github.com/promptfoo/promptfoo/blob/main/site/docs/configuration/guide.mdRepositoryPrimary
- 12.github.com/promptfoo/promptfoo/blob/main/site/docs/usage/command-line.mdRepositoryPrimary
- 13.github.com/promptfoo/promptfoo/blob/main/site/docs/configuration/caching.mdRepositoryPrimary
- 14.promptfoo.dev/docs/integrations/ci-cdWeb
Spotted an error? See our corrections policy.
Enjoyed this? Get the weekly brief.
New guides, Project & Tool of the Week and the AI developments that matter. One email, no spam.
Read next
- TutorialEvaluation & observability
How to evaluate a RAG pipeline with Ragas: metrics, a test set and a first run
Set up RAG evaluation with Ragas: build a small test set, match each metric to retrieval or generation, run a first score and compare two versions.
7 min read
- TutorialEvaluation & observability
Tracing an LLM app with Langfuse: setup, traces, costs and redaction
Add Langfuse tracing to a Python LLM app, choose cloud or self-hosted, read traces and token costs, and mask personal data before it leaves your app.
7 min read
- Weekly digest
The week on HuiaHub: sandboxed agents, RAG tooling and local models
41 new HuiaHub pages this week: sandboxing coding agents, RAG retrieval, local models on Macs and phones, Claude Haiku 5.5 and six how-to guides.
4 min read