Skip to main content
An environment is where the agent acts. A task is the work you measure there: the unit of evaluation, one prompt paired with one graded outcome that resolves to a single number, the reward. That one number is what an evaluation reports and what training learns from. A task has two representations. You author it as an async generator decorated with @env.template, the shape that prompts the agent and scores what happens. Calling that template mints a Task: a plain data row (an env name, a template id, bound args) that travels over the wire and runs anywhere. The agent loop and grading happen in the gap between the generator’s two yields, and a call to run() executes one row and returns a Job.

Defining a task

A task is an async generator with exactly two yields. The first yield is the prompt; the generator pauses while the agent works; the agent’s answer comes back into the generator; the second yield is the reward (0.0-1.0).
tasks.py
This shape is deliberate. Everything the agent does - every step, tool call, and observation - happens in the gap between the two yields, so you describe a task as plain Python: ask on one line, score on the next. The agent loop in the middle is HUD’s job, not yours. The second yield can be a bare float, an EvaluationResult, or a dict with a numeric score; the env layer normalizes any of them to the reward.

Template

@env.template() registers the generator as a template: a callable that mints a concrete Task without running anything.
It takes four optional arguments: Declare returns=T and the answer arrives as a parsed Answer[T] (.content parsed, .raw the original string); without it, answer is the raw string the agent submitted. One template spans a space of tasks. Call it with different arguments and a single function becomes a whole dataset, with no separate artifact per task:
tasks.py
Parameterize across difficulties, inputs, or seeds and one definition becomes an entire benchmark.

Task

A Task is a Pydantic model - one portable, validated row of data. It holds no live environment: env is a name, the join key between the row and whatever brings that environment up at run time. So a task runs anywhere without an env object in-process - the prompt and reward arrive over the wire from the substrate that placement brings up. When you don’t have the template in hand (data pipelines, generated rows), build the model directly - the model is the row, so task.model_dump() and Task.model_validate(data) are the whole codec:

Grading

The second yield is the reward. Return a float inline, or reach for the comparison helpers, async graders, and composed grades documented in graders. For how to design a reward that separates good work from bad and resists hacking, see designing tasks.

Tasksets

A Taskset is a named collection of task rows. Build one in code, or load it from a source:
Write rows back out with ts.to_file("tasks.json") (or .jsonl). Tasksets are also ordered collections:

Running

taskset.run(agent, ...) executes every task and returns a Job. task.run(...) is the same call over a taskset of one, with identical semantics.
A crashed rollout comes back as a failed Run inside the job rather than raising, so one bad rollout never collapses a batch. For the end-to-end workflow see running an eval.

Syncing

Sync publishes a locally-authored taskset to hud.ai so you can run it there, compare models on it, and browse its traces; local runs never need it. The hud sync tasks <name> command uploads a taskset and only what changed, a workflow covered in creating an environment. In code, diff(local, remote) returns a SyncPlan describing the comparison:
Once a reward separates good work from bad, designing tasks covers the difficulty and spread that make a taskset worth training on, and graders the scoring that produces each reward.