Task Folder Patterns
A task folder describes one task. These patterns cover what a dataset needs once it grows past a few hand-written
folders. Each has a committed example under environments/ to copy.
For a dataset of independent hand-written tasks, start from environments/hello_dataset. Every example ships a
reference solution, so you can check it end to end:
Parameterized tasks
When tasks differ only in a few values, such as a question and its answer, write one template folder and one JSONL
row per task. The template is a task folder with tasks.jsonl beside its task.toml:
Each row names the task and gives its fields:
Prepare renders one plain task folder per row into results/harbor/<taskset>/rendered/<task_id>/, for both
gym eval run and gym dataset validate:
- A file named
<stem>.template<suffix>has its{{ task_data.<key> }}and{{ <key> }}placeholders filled and is written as<stem><suffix>. Strings are inserted as they are and other values as JSON (true,null), so put a value that must survive shell quoting in a text file, not inside a quoted string of asolve.template.sh. - Every other file is copied, apart from
tasks.jsonl, the rootREADME.md, prepare’s records, hidden root entries other than.gitignore, and byte-code caches or editor leftovers.dataset.toml,task_data.pyand a rootverifier.pyare copied once, beside the rendered folders, not into each one. - The row’s fields become the task’s
task_data.json. - When the template’s
task.tomlsets[task].name, which must be<org>/<name>, each rendered[task].namebecomes<org>/<task_id>, so every task has its own name under Harbor. Eachtask_idmust then start with a letter or digit and hold only letters, digits,.,_and-.
Prepare fails, naming the task or the file, for a placeholder that names no field, any other {{ ... }} left in a
rendered file, a duplicate or invalid task_id, or a row that TaskData rejects. It also fails for a symbolic link
in the template, a task_data.json at the template root, or two files that render to one name.
The rendered solution/answer.md is a reference answer. When the task has no solution/solve.sh, the oracle writes
it to answer.md in the working directory, where the verifier reads it (see
Reference answers).
To start a template from scratch:
Use separate task folders instead when tasks need their own files, dependencies, tools or scoring logic.
Typed task fields
A task can carry its own field values in task_data.json, a JSON object in its folder. A parameterized task gets
one from its row. The values travel in the task’s row under task_input.task_data, and a Python verifier receives
them as its task_data argument.
A task_data.py beside dataset.toml types those fields. It exports one Pydantic model named TaskData:
Gym checks every task against the model and names the task in the error:
- At prepare.
gym eval runandgym dataset validatecheck every task before any image is recorded or built. One task that fails refuses the whole dataset, even one that--exclude-tasksor--only-tasksleaves out. - At seed. The
harborEnvironment Server refuses a row the model rejects, or whose fields differ from the task folder’stask_data.json. The failure is not retried. - At collate.
gym dataset collatechecks the rows again. Intrain_preparationmode it only reports a rejected row unless you pass+task_data_validation=error.
The check is strict. Unknown keys always fail, whatever the model’s extra setting. Values are checked as JSON, so
"3" is not an int. Keys that start with _ng_ are reserved for Gym. The module is imported on the host, so keep
it to the model.
A task with its own MCP tool
A task can declare tool servers with the Model Context Protocol (MCP). environments/hello_mcp_tool ships a stdio
server in its image and declares it in task.toml:
The harbor Environment Server writes the list to /tmp/.nemo-gym/task.json in the agent’s sandbox. A harness that
starts task tool servers itself, such as miniswe_sandboxed_agent, reads it there. Any other harness is refused for
the task with a terminal failure that names the reason, rather than run without the tool. The oracle of
gym dataset validate always takes the task.
A harness that does not start servers itself can take a task whose MCP servers are all streamable-http with a url,
and that has no skills_dir, when you set task_mcp_grants: required on the server (see
Configure the server). The server then grants the tools to the
agent’s session.
The example’s Dockerfile has RUN and COPY lines, so prepare builds it. With --sandbox docker, Gym builds it into
the local Docker daemon. With OpenSandbox, the image must be pushed to a registry, so set builds.repository (see
Harbor image builds).
Shared verifier code
Keep scoring logic in one place in one of three ways.
Gym’s verifier helpers. nemo_gym.verifiers (final_text, exact_match, numeric_match) and
nemo_gym.verifiers.files (read_text, file_exists, text_file_equals) are copied into the sandbox with every
Python verifier, so the task image needs no Gym package, only python3 or python 3.8 or newer.
environments/hello_verifier_reuse uses them both ways. One task reuses a complete verifier with its own settings:
The other writes its own check around a helper:
One dataset-level verifier. A verifier.py beside dataset.toml grades every single-step task that has no
tests/ folder, and its content enters those tasks’ digests. Only that one file is uploaded, so it can import the
helpers above but no other file of the dataset.
One template. A parameterized dataset shares its tests/verifier.py by construction, and each task’s values reach
it as task_data.
A task’s own tests/verifier.py can also import other modules from its tests/ folder.
Python verifiers lists the arguments verify can declare and
what it may return.