Inngest Agent Evals
Use this skill when the user wants to evaluate AI agents or AI workflows in
production, add scoring, compare prompts/models/tools, group related runs, or
debug why an agent outcome was good or bad.
Agent Evals is not a separate package. It is the production evaluation workflow
built from Inngest functions, durable steps, scores, deferred scorers, sessions,
traces, experiments, and Insights.
Canonical References
Check the public docs first when exact API details matter:
Decision Flow
Start from the outcome, not the mechanism:
- Identify the product or quality signal: helpful click, ticket resolution,
conversion, guardrail pass, retrieval quality, model confidence, latency,
cost, human review, or LLM-as-judge result.
- Decide when the signal appears:
- During the run: use direct scoring with or
.
- After the run: use deferred scoring with and ,
or explicit .
- Decide how humans will inspect related work:
- Use for repeated high-cardinality identifiers such as
, , , or .
- Use Insights for ad hoc historical analysis, low-cardinality filters, SQL
aggregates, and broad investigation.
- Decide whether to compare variants:
- Use for prompt, model, provider, tool, workflow, or
rollout comparisons.
- Pass to deferred or later scores when the score should be
credited to the selected variant.
- Preserve traceability:
- Keep model calls, tool calls, waits, database writes, and scoring work in
durable steps so traces explain why the score happened.
Setup Requirements
- Use TypeScript SDK v4 and install the latest SDK:
npm install inngest@latest
.
- Scoring and deferred scoring are beta APIs; verify current imports against
the docs before shipping user-facing examples.
- requires from on
the Inngest client.
- is imported from and the scorer must
be registered in the serve handler with other functions.
- Sessions sent through the TypeScript SDK require v4.7.0 or later.
- Step experiments require v4.8.0 or later for and the
helper from .
Client shape:
typescript
import { Inngest } from "inngest";
import { scoreMiddleware } from "inngest/experimental";
export const inngest = new Inngest({
id: "support-agent",
middleware: [scoreMiddleware()],
});
Sessions
Add sessions when events belong to a user flow, conversation, ticket, import,
or agent task that someone will inspect repeatedly.
typescript
await inngest.send({
name: "support/ticket.created",
data: {
ticketId: "tk_123",
message: "I can't sign in.",
},
meta: {
sessions: {
ticket_id: "tk_123",
},
},
});
Rules:
- Use stable, non-secret, high-cardinality IDs.
- Keep the key generic, such as ; put the actual ID in the
value.
- Pass sessions explicitly through and when
downstream runs should join the same session.
- Do not use sessions for labels like ; use Insights for
that style of filtering.
Direct Scoring
Use direct scoring when the score is known during the function run.
Good direct scores:
- guardrail pass/fail
- JSON validity
- retrieval confidence
- tool success
- model confidence
- inline LLM-as-judge result
typescript
export const answerTicket = inngest.createFunction(
{
id: "answer-ticket",
triggers: [{ event: "support/ticket.created" }],
},
async ({ event, step }) => {
const answer = await step.run("generate-answer", () =>
generateAnswer(event.data.ticketId)
);
const passed = await step.run("check-answer", () => validateAnswer(answer));
await step.score("score-answer-quality", {
name: "answer-quality",
value: passed,
});
return { answer, passed };
}
);
Use stable score names. Changing a score name creates a separate metric.
Score values must be finite numbers or booleans.
Deferred Scoring
Use deferred scoring when the useful signal arrives after the parent workflow
finishes, such as user feedback, conversion, ticket reopen, retention, or a
slow LLM-as-judge run.
typescript
import { createScorer } from "inngest/experimental";
import { z } from "zod";
export const feedbackScorer = createScorer(
inngest,
{
id: "support-feedback-scorer",
schema: z.object({ ticketId: z.string() }),
},
async ({ event, step }) => {
const feedback = await step.waitForEvent("wait-for-feedback", {
event: "support/feedback.received",
timeout: "7d",
if: `async.data.ticketId == '${event.data.ticketId}'`,
});
return {
name: "user-feedback",
value: feedback?.data.helpful ? 1 : 0,
};
}
);
Trigger it from the producing function:
typescript
async ({ event, step, defer }) => {
const answer = await step.run("generate-answer", () =>
generateAnswer(event.data.ticketId)
);
defer("score-feedback", {
function: feedbackScorer,
data: { ticketId: event.data.ticketId },
});
return { answer };
}
Guardrails:
- Register scorers with
serve({ functions: [...] })
.
- is fire-and-forget; do not it.
- The parent run is attributed automatically for deferred scorers.
- Return a default score or when a signal times out, based on the
product semantics.
Experiments
Use
when comparing prompts, models, providers, tools,
workflow rewrites, or operational settings against real traffic.
typescript
import { experiment } from "inngest";
const { result, variant, experimentRef } = await group.experiment(
"answer-style",
{
variants: {
concise: () => step.run("answer-concise", () => answerConcise(event.data)),
detailed: () =>
step.run("answer-detailed", () => answerDetailed(event.data)),
},
select: experiment.bucket(event.data.accountId, {
weights: { concise: 50, detailed: 50 },
}),
}
);
Selection strategy:
- for run-level traffic splits.
experiment.bucket(stableId, { weights })
when a user, account, or tenant
should usually keep the same experience.
- when assignment comes from a database, flag service, or
rollout table.
- to force one variant during testing or after choosing a
winner.
Rules:
- Each variant callback must call at least one tool.
- Keep experiment IDs and variant names stable because they appear in traces.
- The selected variant is memoized for retries and replays.
- Persist and the parent run ID if a later, separate process
will score the selected variant.
When deferred scoring a variant, pass the ref:
typescript
defer("score-answer-feedback", {
function: feedbackScorer,
data: { ticketId: event.data.ticketId },
experiment: experimentRef,
});
When scoring from a later run explicitly:
typescript
await inngest.score.experiment({
name: "clickthrough",
value: 1,
experiment: experimentRef,
runId: originalRunId,
});
Brownfield Migration
When adding Agent Evals to an existing app:
- Search for existing agent/workflow outputs and where users act on them.
- Find durable identifiers already in the domain: conversation ID, ticket ID,
account ID, import ID, run ID, or recommendation ID.
- Add at event producers before building dashboards around
correlation.
- Add one direct score where the outcome is already known.
- Add a deferred scorer only after the signal event exists or can be emitted
cleanly.
- Add only around one isolated decision at a time.
- Keep old prompt/model/tool behavior stable until scoring proves the new path.
Use
first when the repo has many candidate
workflows. Use
with this skill when the work is an AgentKit or
durable agent workflow.
Anti-Patterns
- Starting with a prompt experiment before naming the outcome metric.
- Scoring with unstable names, strings, objects, , or .
- Forgetting and then assuming is missing.
- Running scorers in an external queue when the scorer needs durable waits.
- Keeping experiment assignment only in memory.
- Scoring an experiment variant from a later run without the original .
- Using sessions for low-cardinality labels or sensitive personal data.
- Leaving model/tool work outside steps, making traces unable to explain the
score.
Verification
- Typecheck the Inngest client, functions, scorer schemas, and serve handler.
- Confirm scorers are registered with the serve endpoint.
- Test the producer emits events with expected .
- Test direct scores use stable names and finite number/boolean values.
- Test deferred scorer timeout behavior.
- For experiments, test that each variant callback contains step work and that
later scoring receives plus the original run ID when needed.
- If possible, run the Inngest dev server and inspect traces, sessions, scores,
and experiment variant selection in the dashboard or through available CLI/API
tooling.