Skip to main content
The CLI wraps these functions; both use the same specs and store. For a single row, start with judge:
This uses the minimal spec. An uncached call asks the configured engine and may incur a charge; a repeated call with the same state uses the stored answer. The rest of this page lists the exact API for single rows, batches and Python-generated specs.

Projects

A project is one or more judgments in dependency order.

load

Returns a project. base: where relative source paths in a dict spec resolve. Ignored for paths.

run

hunch run: asks what the store lacks, materializes each table, prints the same lines, returns the project.

results

A judgment’s table from its last complete run, one dict per row. Default: the last judgment in dependency order. Columns: Store. The table belongs to whichever spec with that judgment name ran last. If that wasn’t this spec as it is now (it was edited since, or another spec shares the name), results raises SystemExit rather than return another spec’s answers, and names the hunch run that writes the table for this one; with the answers cached, that run is free. A judgment that has never run raises too.

Judging one row

judge

Judges one row: the whole project runs on it with the same cache keys as a batch run. Per question it returns: The shape depends on the project:
  • One judgment, or node given: {question: answer}.
  • Several judgments: {judgment: {question: answer}}. A judgment whose where excluded this row is None. A union judgment is left out.
  • A type: multi question appears as one yes/no answer per option, named <question>__<option>.
judge uses asyncio.run; inside a running loop use ajudge. With shadow, the candidate runs in a non-daemon thread after judge returns, so a script waits for it at exit.

ajudge

Async judge: same arguments and return. A shadow candidate runs as a background task in the current loop; its failures go to stderr, never raised.

Costs and errors

Both ask the engine for answers the store lacks, except:
  • A cost cap. HUNCH_MAX_COST (read when hunch is imported), or hunch.settings.MAX_COST = 0.0 at runtime. If one judgment’s missing answers would cost more, nothing is asked and SystemExit is raised with a message such as intent: would ask 1 answers in 1 requests (~$0.0001), above --max-cost $0.0; nothing asked. The cap holds on what is charged: while asking, a request is sent only if its worst case still fits, and one that doesn’t stops the call with SystemExit (… stopped at --max-cost …), the answers already asked saved.
  • A missing input column raises KeyError, for example "command_guard: state needs ['command']".
Spec errors also raise SystemExit; catch it around judge in long-running code.

Pydantic classes as specs

Needs the pydantic extra. Each field becomes one question:

spec_from_model

A spec dict whose questions are the class’s fields. cls may also be a bare output type such as Literal["spam", "ham"] or bool. That becomes one question named output, asked with description. judgment defaults to the class name in snake case. Any other spec key (tests, redact, where, …) can be passed through **spec.

spec_from_agent

A spec dict whose questions are the ones a Pydantic AI agent (2.50 or later, on a decision model) sends, recorded from Pydantic AI with no model call. The agent runs once on a recording model, with deps if its instructions need them, and stops at its first request. Question names are Pydantic AI’s with . written as __: a list field topics becomes one yes/no question per option, topics__refund, topics__login. act, gold and escalate still go in Field(json_schema_extra={"hunch": {...}}) and apply to every question of that field; they are not sent. judgment defaults to the output class name in snake case (agent for a bare output type). Raises ValueError when the agent asks a route question first (several output types, or tools), has a system_prompt, or has instructions that depend on the prompt (it is recorded twice, with different prompts, to tell), and TypeError when state is not a string. Works from async code: the recording runs on a thread of its own.

judge_model

Judges one row with a spec built by spec_from_model and returns an instance of cls. For a bare output type it returns the value itself.

to_model

Builds an instance of cls from answers shaped {question: {"label": ...}}, converting each label to the type the field declares: bool, the Literal or Enum value, a list for list[...], the IntEnum level, None for “none of these”.

SQL

Needs the sql extra: uv add "hunch-ai[sql]". The guide walks through it.

hunch.sql.register

Adds the spec or project folder at path to the DuckDB connection con as a function, and returns its name: the judgment’s, or the folder’s for a project, unless name is given. The function takes the columns the specs read (every judgment’s state, then the columns its where reads, less the answers upstream judgments add), as VARCHAR, in order; hunch.sql.columns(hunch.load(path)) lists them. It returns a struct of questions, each {label, p, route}, or for a project a struct of judgments, NULL where a row never reached one. max_cost is the most the function may be charged in total on this connection, across every judgment and escalation, in USD; default HUNCH_MAX_COST, else no cap. A hunch.sql.Budget(usd) passed to several register calls is one budget they share. At the cap, the query fails (DuckDB raises InvalidInputException with hunch’s message) and the answers already asked are saved.

dbt

hunch.dbt is a dbt-duckdb plugin: module: hunch.dbt with config: {specs: [...], max_cost: USD} registers each spec on every connection dbt opens, all drawing from one budget.

Also exported

Everything else (hunch.spec, hunch.execute, hunch.measure and the other modules) is internal and not a stable API. Run settings are hunch.settings.