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. When authoritative grading belongs in another environment, set Task.verifier to a second task row. Its declaration, placement order, and phase boundary are documented in Verifier environments.

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.
agent_config.timeout_seconds bounds only the agent phase. If it expires, the run is marked as timed out but grading still runs against any completed work. rollout_timeout instead bounds submission, queueing, startup, the agent, and grading together. It is not a wall-clock bound on Task.run or Taskset.run returning: after expiry, local execution may wait up to two seconds for cancellation and still reports the trace, and Taskset.run can spend up to 120 seconds flushing telemetry. Runtime cleanup may finish in the background. When both are set, the agent timeout must be less than the rollout timeout and any explicit actor run_timeout_s. Limits imposed by the selected runtime still apply when rollout_timeout is omitted. Grader-specific limits, such as BashGrader.timeout_seconds, remain local to that grader. 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: To create the taskset outside the team default Project, select a directory Project first or pass an explicit destination:
See Projects for placement precedence and existing-resource behavior.
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.