Generated banner for Creating Online Evaluations
Generated logo for Creating Online Evaluations

Creating Online Evaluations

PostHogOfficialPublished by PostHog

  • Skill
  • Workflow
  • Data and AI
  • MIT
  • No issues found

From the author

Author continuously-running online evaluations in PostHog AI observability, grounded in real failure modes you've identified. Use when the user wants evaluations that automatically score new generations or whole traces going forward — "create an eval to catch X", "continuously check that responses do Y", "turn these failures into evals", or after reading traces finds a recurring failure. Covers letting the explored data decide how many evals to create, proposing that set for the user to pick, choosing the target and eval type (hog / llm_judge / sentiment), configuring an llm_judge's provider and model, scoping which generations trigger it via conditions, creating disabled, verifying scope, enabling, and backfilling past data. Proposes a sentiment eval when no failure mode is worth catching. Finding and ranking the failure modes worth evaluating is its own job — use exploring-ai-failures first. To debug or manage evaluations that already exist, use exploring-llm-evaluations.
— the description by PostHog

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill guides you through the process of creating and deploying online evaluations in PostHog AI observability. It focuses on transforming specific, observed failure modes from your production traffic into automated checks that score new generations or traces. The workflow emphasizes proposing a candidate set of evaluations to the user, verifying their scope to ensure accurate and cost-effective monitoring, and managing the lifecycle of these evaluations from draft to active status.

What it does

  • Converts identified failure modes into scoped, automated online evaluations.
  • Supports multiple evaluation types including Hog (rule-based), LLM-judge, and sentiment analysis.
  • Allows targeting of individual generations, full traces, or entire sessions.
  • Provides tools to dry-run evaluations and verify trigger conditions before activation.
  • Manages the configuration of LLM judges, including provider and model selection.
  • Facilitates backfilling evaluations against historical data.

When to use it

  • When you have identified recurring failure modes in your AI application and want to monitor them automatically.
  • When you need to ensure new AI generations or traces meet specific quality or functional criteria.
  • When you want to implement sentiment analysis to track user frustration.
  • When you need to validate the effectiveness of prompt or code changes by tracking pass rates.

Advantages

  • Reduces manual monitoring by automating the detection of known failure patterns.
  • Ensures evaluations are grounded in real production data rather than generic metrics.
  • Provides granular control over evaluation scope to manage costs and noise.
  • Enables proactive alerting on specific, high-impact failure modes.

Usage examples

“Create an eval to catch when the agent refuses to answer questions it should know.”
The agent will analyze recent traces to define the failure mode, propose a candidate evaluation, and guide you through configuring and enabling it.
“Continuously check that all order lookup responses include an order_id.”
The agent will help you set up a Hog-based evaluation with a boolean output type to flag any generation missing the required ID.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/posthog/creating-online-evaluations/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip creating-online-evaluations.zip -d .agents/skills

Documentation by PostHog

Creating online evaluations

An online evaluation automatically scores either each matching $ai_generation or the whole trace containing it, until disabled. A good eval comes from a real failure mode you've found in production traffic, not from a guess or a generic metric like "hallucination" or "helpfulness". This skill starts once those failure modes are identified and turns them into scoped, continuously-running evals.

One eval per failure mode, and as many evals as the data justifies. How many to create is a judgment call you make from what the traces actually showed — sometimes one, often three or four. Never assume the answer is one, and never bundle unrelated modes into one evaluator. The exception is a set of modes that are outcomes of the same check, such as the reasons a reply missed the user's request: one categorical eval with a category per mode fits that (2.1).

Propose before you create. Bring the user a short list of candidate evals and let them pick which ones they want (Phase 1.1). Creating evals they didn't ask for costs them money and noise.

First, know what you're evaluating. Finding and ranking the failure modes worth catching is a separate job. If the user doesn't specify what they want to evaluate, ask them. If they are still vague about it and don't refer to a specific failure mode, run exploring-ai-failures to scope a use case, find failing traces, and produce a ranked list of failure modes.

For the mechanics of writing and iterating an evaluator (Hog source vs LLM-judge prompt, dry-running, debugging a live eval), defer to exploring-llm-evaluations.

Tools

ToolPurpose
posthog:llma-evaluation-config-getCheck the active provider key used by unpinned judges
posthog:llma-provider-key-listFind a usable (ok state) provider key to pin
posthog:llma-evaluation-judge-modelsList valid provider+model combos
posthog:llma-evaluation-directory-listList directories available for organizing the evaluation
posthog:llma-evaluation-directory-createCreate a directory when the user asks for a new one
posthog:llma-evaluation-test-hogDry-run Hog source against recent generations before creating
posthog:llma-evaluation-createCreate the evaluation (always enabled: false first)
posthog:llma-evaluation-runSpot-run a draft eval against one generation
posthog:llma-evaluation-updateIterate config, then flip enabled: true
posthog:llma-evaluation-backfill-estimateCount what a backfill over past data would evaluate
posthog:llma-evaluation-backfill-createEvaluate past data, only after the user confirms the estimate
posthog:llma-evaluation-backfill-getTrack a backfill's progress and coverage
posthog:llma-evaluation-backfill-listFind an evaluation's backfills, including one already running
posthog:llma-evaluation-backfill-cancelStop a running backfill when the user asks
posthog:execute-sqlVerify a condition matches the events and volume you expect
posthog:generate-app-urlBuild a region- and project-qualified deep link to the eval

The full create payload (every field, the config schemas, the exact conditions shape) is in references/evaluation-payload.md.

Phase 1 — Decide what to propose, then let the user choose

Start from real, observed failures, not metrics you picked in advance. If you don't already have them, run exploring-ai-failures to scope a use case, find failing traces, and produce a ranked list of failure modes — then come back.

1.1 — Turn the failure modes into a candidate set

Let the data decide how many. One failure mode is one eval, so a ranked list of four distinct modes is a candidate set of four evals. Don't collapse them into one evaluator that tries to catch everything, and don't stop at the top mode when the traces clearly showed more worth watching. Keep a candidate when:

  • It hurts. Frequent or painful. A handful of modes usually covers the majority of failures.

  • It's checkable. It reduces to one crisp criterion — "the reply must stay on the user's topic", "the tool call must include an order_id". If you can't state it in a line, it isn't ready to propose.

  • It's distinct. Two candidates that would fail on the same generations are one eval.

Rank by how much they hurt and propose roughly the top five; mention in a line that you set weaker ones aside rather than silently dropping them.

1.2 — Propose the set in plain language

Never create evals the user hasn't picked. Lay out the candidates and ask which ones they want.

Assume they haven't read the traces with you and don't know the eval vocabulary. Keep each one to a line or two — a wall of text per eval means they can't compare them — but make it obvious what it watches and what they'll see when it fails. No Hog snippets, property filters, or hog/llm_judge internals here. Number them so they can reply "1 and 3":

Found 3 failure patterns worth watching. Which should I set up?

1. Replies drift off topic — checks the answer addresses what the user actually asked. Catches support replies that confidently answer a different question. Seen ~40×/day.

2. Order lookups missing an order ID — checks every lookup_order call includes an order_id. Catches the silent tool failures that make the agent invent an order status. Seen ~15×/day.

3. Refuses questions it can answer — checks the reply isn't a needless "I can't help with that". Catches users being turned away from things the docs cover. Costs an LLM call per check.

Per eval: what it checks, what bad thing it surfaces, and rough volume when you know it (Phase 2.5 verifies it properly). Flag the ones needing an LLM judge, since those cost per run — that's the only mechanic worth exposing up front. Keep the ranking implicit in the order; skip scoring tables.

Recommend a starting point if they seem unsure (usually the top one or two), and treat "all of them" or "go ahead" as accepting the whole set. If a prompt tweak would likely fix a mode, suggest the fix alongside the eval rather than in place of it — a rising pass rate is how they confirm the fix landed.

1.3 — When nothing surfaced, propose sentiment

If the traces were read and no failure mode is worth an eval, don't invent a failure and don't come back empty-handed. Say what you looked at, then propose a sentiment eval as the floor:

No clear failure pattern in the last 7 days. Worth starting with:

1. User frustration — labels each user message positive, neutral, or negative. Shows which conversations are going badly, which is usually where the real failures hide. No judge cost.

It needs no provider key, it's cheap, and it gives them a signal to come back to once there's enough traffic to spot patterns.

No generations means no eval of any kind. Sentiment only scores matching $ai_generation events, so if the project has none, every eval you could create — sentiment included — would sit there never firing. Don't propose one. Point them at instrument-llm-analytics to get AI observability capturing generations first, and come back to this skill once there's traffic to read.

Phase 2 — Build each accepted eval

Run 2.1 through 2.5 once per eval the user picked, so each one lands as a verified draft. Leave every one of them disabled until the whole set is verified — 2.6 is a single pass over the finished set at the end, not the last step of each loop. Enabling eval 1 while eval 3 is still being written puts a partially live set into production, which is noise and (for a judge) cost the user didn't agree to yet.

2.1 — Choose the eval type
Use…When the criterion is…
hogStructural / rule-based (JSON parses, length, regex, tool-call shape). Cheap, deterministic, no provider key needed.
llm_judgeSubjective / fuzzy (tone, factuality, on-topic). Costs an LLM call per run; needs a provider and model. A usable provider key is only needed to enable it, not to create a draft.
sentimentYou want sentiment labels on user messages, not a pass/fail (unless very specifically asked for, usually not relevant to this skill).

Reach for hog first, escalate to llm_judge if there is no deterministic way to check for what we want to check.

Then choose what one run returns. hog and llm_judge support all three output types:

output_typeWhen the failure mode is…
booleanOne yes/no criterion. The default. Set true_is_failure: true when the check detects a problem.
numericA matter of degree (a 1–5 quality rating) or a measured value (length, latency, cost).
categoricalOne check whose outcome falls into named buckets, such as which of several related failure modes happened.
  • Numeric: set min and max so out-of-range scores are skipped, not graded. For pass rates and reports, add a passing_rule (gte or lte a threshold). step is only a hint: a judge can return a fractional score such as 3.7 with step: 1, and the score is stored as returned.

  • Categorical: define options as stable lowercase keys with display labels. Use single selection when exactly one outcome applies, and multiple when several problems can co-occur. For pass rates and reports, set passing_rule.categories to the keys that count as a pass. In single mode that is usually a "no problem" key. In multiple mode, define no "no problem" key and set categories: [], so only an empty result passes. Any listed key makes an empty result fail.

  • Without a passing rule, numeric and categorical results are still recorded, but there are no pass rates or reports.

When an llm_judge returns numeric or categorical, PostHog tells the judge the output format. The prompt only describes what each score or category means. Full config fields: references/evaluation-payload.md.

2.2 — Choose the target
TargetBehavior
generationRuns once for each matching $ai_generation, immediately after ingestion. This is the default.
traceRuns once for the whole trace after the first matching generation and a configurable wait for the trace to finish.
sessionRuns once for the whole $ai_session_id session, after the session settles.

For a trace target, send "target": "trace" plus a settle config that controls when the trace is evaluated, discriminated on strategy:

  • { "strategy": "fixed_window", "window_seconds": 1800 } — evaluate a fixed wait after the first matching generation. Between 10 seconds and 2 hours, defaults to 30 minutes. A target_config without a strategy key means this.

  • { "strategy": "inactivity", "quiet_period_seconds": 300, "max_age_seconds": 7200 } — evaluate once the trace has had no new activity for the quiet period (10 seconds to 30 minutes, defaults to 5 minutes). max_age_seconds caps the total wait from the first matching generation (1 minute to 2 hours, defaults to 2 hours, must be at least the quiet period).

A session target takes the same settle config with session-sized bounds, and defaults to inactivity rather than fixed_window:

  • { "strategy": "inactivity", "quiet_period_seconds": 3600, "max_age_seconds": 86400 } — evaluate once the session has had no new activity for the quiet period (10 seconds to 24 hours, defaults to 1 hour). max_age_seconds caps the total wait from the first matching generation (1 minute to 7 days, defaults to 24 hours, must be at least the quiet period).

  • { "strategy": "fixed_window", "window_seconds": 1800 } — evaluate a fixed wait after the first matching generation (10 seconds to 7 days).

A session evaluation only fires for events that carry $ai_session_id. Producers either set it on every generation or on none, so an SDK that does not set it will never trigger a session evaluation. $ai_session_id is not $session_id: the second is PostHog's product-analytics session and is unrelated.

A session evaluation can also come back skipped rather than graded. The emitted $ai_evaluation event then carries $ai_evaluation_skipped: true and an $ai_evaluation_skip_reason, and its $ai_evaluation_result is false when the evaluation disallows N/A, so any analysis of pass rates has to exclude skipped runs rather than count them as failures. Sessions are skipped when they hold more than 2500 events (usually a session id shared across conversations), when nothing was found in the evaluation window, and, for an LLM judge, when the transcript is too long to send in full.

A session is evaluated at most once per evaluation, for as long as the completed run stays inside Temporal's retention window. A session that resumes long after being evaluated may be evaluated again, so pick a quiet period long enough that the session is really finished. A longer quiet period costs only latency.

Conditions still match the generation that triggers the run; the evaluator itself receives the complete trace or session. Sentiment evaluations support only the generation target.

New Hog source should use the globals shared by all targets:

GlobalMeaning
evaluation_eventsOne generation event for a generation target, or every captured event for a trace or session target.
targetThe target's type, id, total_cost_usd, and total_latency_seconds.
item.input_text / item.output_textBest-effort readable projections; use these for length, keyword, and regex checks.
item.input / item.outputOriginal serialized values; use these when the evaluator needs to parse the captured JSON itself.

For a session target, target.id is the session id, and target.total_cost_usd / target.total_latency_seconds are summed across the session's traces. total_latency_seconds is time spent on AI work, not session wall-clock; the two can differ by orders of magnitude. Session wall-clock is derivable from evaluation_events timestamps.

Generation evaluations still expose top-level input, output, properties, and event. Trace evaluations still expose their original events and trace globals. Those globals are kept for compatibility with saved evaluators. Session evaluations do not carry them: session Hog source only receives target and evaluation_events. Do not use target-specific globals in new source that needs to work across targets. The text projections recognize common provider payloads but are not authoritative; use item.input / item.output when exact structure matters.

2.3 — Configure the LLM judge

An llm_judge evaluation requires only a valid provider and model to create as a draft. A usable provider key is needed to enable it, not to save it, so no ok key is a reason to keep the eval disabled, not a reason to stop. provider_key_id controls whether the evaluation pins one specific key:

  • Set provider_key_id to the UUID of an ok-state key for the same provider to pin it.

  • Set provider_key_id to null to use the team's active provider key. This is always a valid saved value. To enable the eval, the active key must be in the ok state and use the same provider as model_configuration.provider.

Hog and sentiment evaluations skip this step.

posthog:llma-evaluation-config-get        // check active_provider_key for an unpinned judge
posthog:llma-provider-key-list            // find an ok-state key to pin
posthog:llma-evaluation-judge-models      // {} → every provider and its models; { "provider": "openai" } narrows it

Confirm the provider and model with llma-evaluation-judge-models. Call it with no arguments to see the whole catalog at once. Providers PostHog funds no models for come back empty unless you pass key_id for one of the team's keys; the response's providers list flags which ones those are. A listed model is not a free model. Running any llm_judge needs one of the team's own provider keys, so enabling fails until a usable key exists. Prefer pinning the chosen key so a later team-wide active-key change does not change how the evaluation runs. Leave provider_key_id as null to run on the team's active key; before enabling, confirm with llma-evaluation-config-get that the active key is usable and its provider matches.

No usable key does not block creating the judge. Keys cannot be added through MCP, so:

  • If the team has an ok key that the judge isn't set to use, pin it with provider_key_id instead of asking for a new one.

  • If the team has no usable key, still create the judge with enabled: false. Do not spot-run or enable it.

  • Tell the user plainly that the judge starts scoring once a project admin adds a key in Settings > AI observability > Bring your own key (BYOK) and enables it. A judge left disabled with no word to the user never runs, so this message is part of the job, including when a create with enabled: true is rejected for a missing key and you retry it disabled.

2.4 — Create it disabled

Create with enabled: false so nothing fires until the scope is verified. Minimal hog example:

Evaluations may be created at the top level or in one directory. If the user names a directory, call posthog:llma-evaluation-directory-list and pass its UUID as directory_id. Create a directory only when the user asks for one. Omit directory_id or pass null for the top level. Directories cannot be nested.

posthog:llma-evaluation-create
{
  "name": "Output is not empty",
  "description": "Fails when a generation has no readable output",
  "evaluation_type": "hog",
  "evaluation_config": { "source": "let count := 0\nfor (let i, item in evaluation_events) {\n    if (item.event == '$ai_generation') {\n        count := count + 1\n        if (length(trim(item.output_text)) == 0) { return false }\n    }\n}\nreturn count > 0" },
  "output_type": "boolean",
  "output_config": { "allows_na": false },
  "target": "generation",
  "target_config": {},
  "conditions": [
    { "id": "default", "rollout_percentage": 100, "properties": [{ "key": "$ai_model", "type": "event", "operator": "icontains", "value": "gpt" }] }
  ],
  "enabled": false
}

For llm_judge, swap evaluation_config to { "prompt": "…" } and add "model_configuration": { "provider": "openai", "model": "gpt-5-mini", "provider_key_id": null }. Pin provider_key_id to the UUID of an ok-state key from llma-provider-key-list to run on one specific key; null runs on the team's active key. Either value saves a draft. Full field reference: references/evaluation-payload.md.

2.5 — Verify the scope before enabling

conditions is where online evals go wrong: too broad and you evaluate (and bill) a firehose; too narrow and it never fires. Confirm the filter matches the events you expect, and roughly how many per day:

posthog:execute-sql
SELECT count() AS matched, count() / 7 AS per_day
FROM events
WHERE event = '$ai_generation'
    AND properties.$ai_model ILIKE '%gpt%'      -- mirror each condition property
    AND timestamp >= now() - INTERVAL 7 DAY

For generation targets, count() is the run volume. For trace targets, count distinct non-empty $ai_trace_id values because matching generations from the same trace schedule only one run.

If volume is high, set rollout_percentage below 100 to sample. Spot-check the evaluator with llma-evaluation-test-hog (hog) or llma-evaluation-run against one generation (llm_judge; skip the spot-run for a judge with no usable key, per 2.3). For a numeric or categorical Hog eval, pass the same output_type and output_config to llma-evaluation-test-hog, so the preview validates the scores or category keys the saved eval will accept. Both tools currently use generation samples; for a trace target they can check shared source or prompt behavior, but they do not reproduce the complete settled trace. Review the first live trace results before increasing rollout.

Watch out: some orgs reuse a single $ai_trace_id across 100k+ events. Scoping by trace-ID prefix can match far more than expected — verify volume with the SQL above before enabling.

2.6 — Enable the verified set, then close the loop

Only once every accepted eval is a scope-verified draft, enable them — one call each:

posthog:llma-evaluation-update
{ "evaluationId": "<uuid>", "enabled": true }

Each now runs on every new matching generation, or once per matching trace for a trace target. This isn't one-and-done: the user should be aware that they need to keep an eye on results and iterate if the outcome is not the expected one. To wire results into a Slack feed, see feature-usage-feed.

If the user asked to see results before anything goes live, show the test results for the whole set and ask once, in the same reply: "Enable these N now?" Enable the ones they approve right away, in this session. Don't end the conversation on a list of drafts: an eval nobody answered about stays disabled and never runs.

Close the loop across the whole set at once — one short list of what's now live with a link each, not a play-by-play per eval. Mention any candidate you left disabled (the user said no, no usable provider key, volume too high to enable yet), what would unblock it, and the link to enable it later.

2.7 — Offer to evaluate past data

An enabled eval only scores traffic from now on. If the user wants results on data they already have, offer a backfill over a recent window, at most the last 30 days. posthog:llma-evaluation-backfill-create refuses a disabled eval, so enable it first (see 2.6).

  1. Call posthog:llma-evaluation-backfill-estimate with the window, conditions, and rerun_existing the user wants. Tell the user total_units, the returned window (it is clamped, so it can differ from the one you asked for), and what each unit costs: one run of the eval, counted as an AI observability event, plus a model call for an llm_judge. already_evaluated_units already have a result and are left out unless rerun_existing is true.

  2. Call posthog:llma-evaluation-backfill-create only after the user says yes. Pass the window_start and window_end the estimate returned, not the ones you asked for, with the same conditions and rerun_existing, so the run matches what they approved.

  3. Track it with posthog:llma-evaluation-backfill-get. Skipped units were already being evaluated by the eval itself, so they are covered, not missed. Once it completes, a remaining_count above zero means units were left without a result; offer another backfill over the same range. Offer it once. If the second run leaves units behind again, the eval keeps failing on them, so tell the user instead of offering a third.

  4. If posthog:llma-evaluation-backfill-create says it could not confirm the start, call posthog:llma-evaluation-backfill-list before trying again, because the backfill may be running.

  5. To stop a backfill, call posthog:llma-evaluation-backfill-cancel. Evaluations it already started still finish.

One backfill runs per evaluation at a time.

Scoping with conditions

conditions is a list of condition sets — OR between sets, AND within a set's properties. Each set is { id, rollout_percentage, properties[] }. There is no time window inside conditions; sampling is only rollout_percentage (0–100). Property filters use the standard PostHog shape (key, type, operator, value). For trace targets, these filters still select the generation that triggers the eventual whole-trace evaluation.

"conditions": [
  { "id": "openai",    "rollout_percentage": 100, "properties": [{"key": "$ai_provider", "type": "event", "operator": "exact", "value": "openai"}] },
  { "id": "anthropic", "rollout_percentage": 25,  "properties": [{"key": "$ai_provider", "type": "event", "operator": "exact", "value": "anthropic"}] }
]

Build links with posthog:generate-app-url — never hand-write the host or the /project/<id>/ prefix. The url must be a canonical catalog template; pass concrete ids via params, never inline them into the path.

  • Evaluations list: generate-app-url {url: "/ai-evals/evaluations"}

  • Single evaluation: generate-app-url {url: "/ai-evals/evaluations/{id}", params: {id: "<evaluation_id>"}}

These resolve to the correct region host and project prefix (e.g. https://us.posthog.com/project/<id>/ai-evals/evaluations/<evaluation_id>). Surface the link after creating so the user can review and toggle it in the UI.

Tips

  • Evals come from real failures, not generic metrics. Start from a failure found in this product's traffic (via exploring-ai-failures), not from "let's measure hallucination". A metric nobody traced back to a real bad output is noise.

  • One eval, one failure mode — and as many evals as the data justifies. Different failure modes need different evals; don't make one eval try to catch everything, and don't default to creating exactly one when the traces showed several modes worth watching. Related modes that one check tells apart are the exception: they fit one categorical eval.

  • Propose, then create what they picked. Show the candidate set in plain language, a line or two each, and wait for the user to choose. Long per-eval write-ups get skimmed, not read.

  • Nothing found still has an answer. Traces read but no failure mode worth an eval means proposing a sentiment eval, not returning empty-handed. No $ai_generation events at all is the exception — no eval can fire, so send them to instrument-llm-analytics instead.

  • Suggest changes along with the eval if possible. If it's clear a prompt change would fix the issue, for instance, set up the eval but also suggest to the user they change the prompt: they should soon see the eval go from low pass rate to a higher pass rate.

  • hog first. No provider key, no AI approval, deterministic. Reach for llm_judge only when the criterion genuinely can't be coded.

  • Always create disabled, verify scope, then enable. An eval firing on the wrong events is worse than none — noise, and (for llm_judge) cost.

  • A judge draft needs only a provider and model. A usable provider key is needed to enable it, not to save it, so no ok key means keep it disabled — not stop. provider_key_id may be null to run on the team's active key.

  • Online evals are for live traffic, not a registry. Don't create disabled online evals as records or placeholders for evals that run elsewhere (CI, a local eval folder, a test suite). Record those as offline experiments (analyzing-offline-evaluations). Every online eval you create should be one you mean to enable.

  • bytecode is server-written for hog evals — never pass it; send only evaluation_config.source.

  • For cluster-scoped evals, identify the cluster with exploring-llm-clusters, then translate its event filter into conditions.

  • exploring-ai-failures — find and rank the failure modes worth evaluating — do this first

  • exploring-llm-evaluations — debug and manage evaluations that already exist

  • exploring-llm-clusters — identify a cluster to scope cluster-targeted eval conditions

Files

references/evaluation-payload.md

Evaluation create payload reference

Full field reference for posthog:llma-evaluation-create. The evaluation_config and output_config schemas below are rendered from the backend Pydantic models at build time, so they can't drift.

Top-level fields

FieldRequiredNotes
nameyesUp to 400 chars.
descriptionnoDefaults to "".
evaluation_typeyes"hog", "llm_judge", or "sentiment".
evaluation_configyesShape depends on evaluation_type (below).
output_typeyes"boolean", "numeric", or "categorical" for hog/llm_judge; "sentiment" for sentiment.
output_configno{ "allows_na": bool, "true_is_failure": bool } for boolean; numeric and categorical settings below; {} for sentiment.
model_configurationllm_judge onlyProvider + model; key ID optional. Rejected on hog/sentiment.
targetno"generation" (default), "trace", or "session". Sentiment supports only "generation".
target_configtrace/session onlySettle config discriminated on strategy (below); defaults to a 30-minute fixed window for trace, and to a 1-hour inactivity window for session.
conditionsnoTrigger condition sets (below). For traces, conditions match the triggering generation.
enablednoDefaults to false. Create disabled, then flip with llma-evaluation-update.

Valid (evaluation_type, output_type) pairs: (hog, boolean), (llm_judge, boolean), (hog, numeric), (llm_judge, numeric), (hog, categorical), (llm_judge, categorical), (sentiment, sentiment). The output type cannot change after creation.

target and target_config

  • "target": "generation" runs once for each matching generation and uses an empty target_config.

  • "target": "trace" runs once for the whole trace. target_config picks the settle strategy:

    • { "strategy": "fixed_window", "window_seconds": 10..7200 } waits a fixed delay after the first matching generation (defaults to 1800). A config without a strategy key means this.

    • { "strategy": "inactivity", "quiet_period_seconds": 10..1800, "max_age_seconds": 60..7200 } evaluates once no new activity arrived for the quiet period (defaults to 300), capped at max_age_seconds (defaults to 7200, must be at least the quiet period) from the first one.

  • "target": "session" runs once for the whole $ai_session_id session, after the session settles. target_config picks the settle strategy with session-sized bounds:

    • { "strategy": "inactivity", "quiet_period_seconds": 10..86400, "max_age_seconds": 60..604800 } evaluates once no new session activity arrived for the quiet period (defaults to 3600), capped at max_age_seconds (defaults to 86400, must be at least the quiet period) from the first one. This is the default strategy for a session target.

    • { "strategy": "fixed_window", "window_seconds": 10..604800 } waits a fixed delay after the first matching generation (defaults to 1800).

    • A session evaluation only fires for events that carry $ai_session_id. Producers set it on every generation or on none, so an SDK that never sets it will never trigger a session evaluation. $ai_session_id is not $session_id, PostHog's product-analytics session; the two are unrelated.

    • A session is evaluated at most once per evaluation, for as long as the completed run stays inside Temporal's retention window. A session that resumes long after being evaluated may be evaluated again.

  • Conditions always match generation properties. For a trace or session target, that matching generation schedules the eventual whole-unit evaluation.

  • Sentiment evaluations cannot use the trace or session target.

evaluation_config by type

llm_judge
{
  "description": "Configuration for LLM judge evaluations",
  "properties": {
    "prompt": {
      "description": "Evaluation criteria prompt",
      "minLength": 1,
      "title": "Prompt",
      "type": "string"
    },
    "judge_method": {
      "anyOf": [
        {
          "enum": [
            "llm",
            "decision"
          ],
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Judge method. Omit to keep the saved method; null restores automatic routing.",
      "title": "Judge Method"
    },
    "include_media": {
      "anyOf": [
        {
          "type": "boolean"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Set to false to judge text only.",
      "title": "Include Media"
    },
    "reasoning_level": {
      "anyOf": [
        {
          "enum": [
            "minimal",
            "low",
            "medium",
            "high"
          ],
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "How much the judge model reasons before it answers. Null uses the model's default.",
      "title": "Reasoning Level"
    }
  },
  "required": [
    "prompt"
  ],
  "title": "LLMJudgeConfig",
  "type": "object"
}
hog
{
  "description": "Configuration for Hog code evaluations",
  "properties": {
    "source": {
      "description": "Hog source code",
      "minLength": 1,
      "title": "Source",
      "type": "string"
    },
    "bytecode": {
      "description": "Compiled bytecode (set automatically on save)",
      "items": {},
      "title": "Bytecode",
      "type": "array"
    }
  },
  "required": [
    "source"
  ],
  "title": "HogEvalConfig",
  "type": "object"
}

bytecode is compiled and written by the server on save — never pass it. Send only source.

sentiment
{
  "description": "Configuration for sentiment evaluations.\n\nThe classifier is an English-trained model, so labels are unreliable for other languages. A\nmultilingual agent should use an llm_judge evaluation instead. See\nposthog/temporal/ai_observability/sentiment/README.md.",
  "properties": {
    "source": {
      "const": "user_messages",
      "default": "user_messages",
      "description": "Text source used for sentiment classification.",
      "title": "Source",
      "type": "string"
    }
  },
  "title": "SentimentEvalConfig",
  "type": "object"
}

output_config

boolean output
{
  "additionalProperties": false,
  "description": "Configuration for boolean output type",
  "properties": {
    "allows_na": {
      "default": false,
      "title": "Allows Na",
      "type": "boolean"
    },
    "true_is_failure": {
      "default": false,
      "title": "True Is Failure",
      "type": "boolean"
    },
    "decision_threshold": {
      "anyOf": [
        {
          "maximum": 1,
          "minimum": 0,
          "type": "number"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "title": "Decision Threshold"
    }
  },
  "title": "BooleanOutputConfig",
  "type": "object"
}

allows_na: true lets the evaluator return N/A in addition to true/false. N/A differs from an execution error that skips a run. true_is_failure: true treats a raw true result as a failure and a raw false result as a pass. Leave it false for the default mapping.

numeric output
{
  "$defs": {
    "NumericPassingRule": {
      "additionalProperties": false,
      "properties": {
        "operator": {
          "enum": [
            "gte",
            "lte"
          ],
          "title": "Operator",
          "type": "string"
        },
        "threshold": {
          "title": "Threshold",
          "type": "number"
        }
      },
      "required": [
        "operator",
        "threshold"
      ],
      "title": "NumericPassingRule",
      "type": "object"
    }
  },
  "additionalProperties": false,
  "properties": {
    "min": {
      "anyOf": [
        {
          "type": "number"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "title": "Min"
    },
    "max": {
      "anyOf": [
        {
          "type": "number"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "title": "Max"
    },
    "step": {
      "anyOf": [
        {
          "exclusiveMinimum": 0,
          "type": "number"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "title": "Step"
    },
    "allows_na": {
      "default": false,
      "title": "Allows Na",
      "type": "boolean"
    },
    "passing_rule": {
      "anyOf": [
        {
          "$ref": "#/$defs/NumericPassingRule"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    }
  },
  "title": "NumericOutputConfig",
  "type": "object"
}

Return a finite number, not a boolean or numeric string. Optional min and max are inclusive; an out-of-bounds score skips that run. step guides scoring without rounding results. allows_na defaults to false; set it to true to allow null from Hog. Set passing_rule to {"operator": "gte", "threshold": 7} (at least 7) or use lte (at most). Without a passing rule, scores remain ungraded and reports are unavailable. Changing the rule reinterprets historical scores; previously generated reports stay unchanged.

categorical output
{
  "$defs": {
    "CategoricalOption": {
      "additionalProperties": false,
      "properties": {
        "key": {
          "maxLength": 128,
          "minLength": 1,
          "pattern": "^[a-z0-9]+(?:[_-][a-z0-9]+)*$",
          "title": "Key",
          "type": "string"
        },
        "label": {
          "maxLength": 256,
          "minLength": 1,
          "title": "Label",
          "type": "string"
        }
      },
      "required": [
        "key",
        "label"
      ],
      "title": "CategoricalOption",
      "type": "object"
    },
    "CategoricalPassingRule": {
      "additionalProperties": false,
      "properties": {
        "categories": {
          "items": {
            "type": "string"
          },
          "title": "Categories",
          "type": "array"
        }
      },
      "required": [
        "categories"
      ],
      "title": "CategoricalPassingRule",
      "type": "object"
    }
  },
  "additionalProperties": false,
  "properties": {
    "options": {
      "items": {
        "$ref": "#/$defs/CategoricalOption"
      },
      "maxItems": 100,
      "minItems": 1,
      "title": "Options",
      "type": "array"
    },
    "selection_mode": {
      "default": "single",
      "enum": [
        "single",
        "multiple"
      ],
      "title": "Selection Mode",
      "type": "string"
    },
    "allows_na": {
      "default": false,
      "title": "Allows Na",
      "type": "boolean"
    },
    "passing_rule": {
      "anyOf": [
        {
          "$ref": "#/$defs/CategoricalPassingRule"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    }
  },
  "required": [
    "options"
  ],
  "title": "CategoricalOutputConfig",
  "type": "object"
}

options holds 1 to 100 {key, label} pairs. Keys are unique lowercase identifiers (a-z, 0-9, _, -) and are what results store; labels are for display. Keep a key stable when you rename its label. selection_mode is single (each result has exactly one key) or multiple (zero or more keys). Hog returns a list of keys, or a single key string in single mode; null is N/A when allows_na is true. Set passing_rule to {"categories": ["no_problem"]}: a result passes when it has at least one key and every key is a passing key, so an empty result fails. In multiple mode, {"categories": []} passes only empty results, which fits detectors where every key is a problem. Without a passing rule, results are recorded but there are no pass rates or reports. Changing the rule reclassifies historical results; previously generated reports stay unchanged.

sentiment output

Empty object: {}.

model_configuration (llm_judge only)

FieldRequiredNotes
provideryesOne of openai, anthropic, gemini, openrouter, fireworks, azure_openai, together_ai.
modelyesModel id, e.g. gpt-5-mini. Validate against llma-evaluation-judge-models.
provider_key_idnoUUID of an ok-state key for the same provider, or null to use the active team key.

provider and model are required. Pin provider_key_id to run on one specific key, or set it to null to run on the team's active key — both save a valid draft. To enable an unpinned eval, the active key must be ok and use the same provider (llma-evaluation-config-get shows it). A disabled draft may be saved without a usable key, but it cannot be tested or enabled until a key can be resolved.

conditions

A list of condition sets. OR between sets, AND within a set's properties. Omitting conditions (or an empty list) matches every $ai_generation. A generation target evaluates each match; a trace target evaluates each matching trace once.

FieldRequiredNotes
idyesStable string identifier for the set (e.g. "default").
rollout_percentageno0–100, defaults to 100. The sampling rate the dispatcher reads.
propertiesnoFlat list of PostHog property filters, AND-ed together.

Each property filter: { "key": "...", "type": "event" | "person", "operator": "...", "value": ... }. Common operators: exact, is_not, icontains, not_icontains, regex, gt, lt, is_set, is_not_set. There is no time/date field inside conditions — scope by event timestamp upstream if needed, and sample volume with rollout_percentage.

"conditions": [
  {
    "id": "gpt-only",
    "rollout_percentage": 50,
    "properties": [
      { "key": "$ai_model", "type": "event", "operator": "icontains", "value": "gpt" },
      { "key": "$ai_is_error", "type": "event", "operator": "exact", "value": ["false"] }
    ]
  }
]

Full examples

Hog (no provider key required)
{
  "name": "Reply is under 2,000 characters",
  "evaluation_type": "hog",
  "evaluation_config": { "source": "for (let i, item in evaluation_events) { if (item.event == '$ai_generation' and length(item.output_text) >= 2000) { return false } } return true" },
  "output_type": "boolean",
  "output_config": { "allows_na": false },
  "target": "generation",
  "target_config": {},
  "conditions": [{ "id": "default", "rollout_percentage": 100, "properties": [] }],
  "enabled": false
}

The same Hog source works for a whole trace. Change only the target fields:

{
  "target": "trace",
  "target_config": { "strategy": "fixed_window", "window_seconds": 1800 }
}

Or evaluate when the trace goes quiet instead of after a fixed delay:

{
  "target": "trace",
  "target_config": { "strategy": "inactivity", "quiet_period_seconds": 300, "max_age_seconds": 7200 }
}

The same Hog source also works for a whole session, once no new activity has arrived for the quiet period:

{
  "target": "session",
  "target_config": { "strategy": "inactivity", "quiet_period_seconds": 3600, "max_age_seconds": 86400 }
}

New Hog source should use the shared evaluation_events and target globals. Top-level generation globals such as input, output, properties, and event, plus the trace-only events and trace globals, remain available for compatibility with saved evaluators. Session evaluations do not carry the legacy events / trace globals: session Hog source only receives evaluation_events and target. item.input_text and item.output_text are best-effort readable projections of common provider payloads; use raw item.input and item.output when exact structure matters.

LLM judge
{
  "name": "Response stays on-topic",
  "description": "Fails if the assistant changes topic from the user's question",
  "evaluation_type": "llm_judge",
  "evaluation_config": { "prompt": "Return true if the assistant's reply stays on the user's topic, false if it changes subject. Return N/A if the user did not ask a question." },
  "output_type": "boolean",
  "output_config": { "allows_na": true },
  "model_configuration": { "provider": "openai", "model": "gpt-5-mini", "provider_key_id": null },
  "target": "generation",
  "target_config": {},
  "conditions": [{ "id": "default", "rollout_percentage": 100, "properties": [] }],
  "enabled": false
}

provider_key_id: null runs on the team's active key and always saves a valid draft. Pin it to an ok-state key uuid from llma-provider-key-list to run on one specific key. To enable an unpinned eval, the team's active key must be ok and its provider openai.

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Security

No issues found by our static scan· No issues found

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
PostHog
Publisher
PostHog (Official)
Source
PostHog/skills · skills/omnibus/creating-online-evaluations · commit a5d30e7
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by PostHog or PostHog.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download creating-online-evaluations.zip