YAML and serialization
Most of a Plural project is plain settings: which model an Agent uses, which Tasks a Benchmark holds, which grader marks a Task. You write those settings in short YAML files, a simple format of key: value lines that is easy to read and review. Anything that has to do something, like the rules of a game, lives in a Python file beside it.
This page is for anyone who edits a project's files directly. The CLI and the Python SDK read the same files and build the same Plural objects from them, so a project works the same way from either.
The smallest useful file is an Agent, a model plus its instructions, in agents/careful/agent.yaml:
name: carefulmodel: openai/gpt-5.6-lunainstructions: Use the available actions and check the result before finishing.One manifest per resource
Each resource is a folder named after it, holding exactly one YAML file named after its kind. That file is the resource's manifest. A project looks like this:
project.yaml project name and descriptionplural.lock written by Plural and updated by push; commit it, do not edit itenvironments/<name>/ environment.yaml, server.py or environment.py, README.md, optional resources/tasks/<name>/ task.yaml, instruction.md, optional resources/verifiers/<name>/ verifier.yaml, verify.pyharnesses/<name>/ harness.yaml, harness.pyagents/<name>/ agent.yamlbenchmarks/<name>/ benchmark.yaml, README.mdYou rarely start from a blank file. plural <kind> init <name> writes each folder from a template, with a comment on every field and a PLURAL-TODO marker wherever you must fill something in. Validation reports every marker that is left, and push and run --record refuse until none are.
Every manifest follows the same rules:
namemust match the folder name.versionis optional. It asks for a release version on the next push, and you never need to change it when you edit a resource: the hosted project numbers each pushed revision itself.- Other resources are referenced by name within the project, such as
environment: support-queue. - Python behavior is referenced as
file.py:Object, relative to the manifest. - Files are referenced by paths relative to the manifest, and every path must stay inside the resource folder.
- Unknown keys are errors, not silently ignored. Optional fields use the same defaults as the Python SDK.
Tip: Download the JSON Schema for every manifest from project-schemas.json to get completion and validation in your editor.
Start with a small Agent
An agent.yaml can contain just the values you want to set, like the careful Agent above. With no harness, the Agent uses native, Plural's built-in tool loop. A Harness is the loop that decides what the model does next. To attach a built-in Harness, name it and set its options:
name: support-claudemodel: anthropic/claude-sonnet-5harness: claude-codeharness_config: reasoning_effort: highCustom Harness behavior stays in Python, in the project's harnesses/ folder. The Agent names that Harness:
name: support-custommodel: openai/gpt-5.6-lunaharness: support-loopCredentials never belong in a manifest. Supply them when the run starts; see Harnesses.
Reference other resources by name
Resources point at each other by name. A Task (one assignment) names its one Environment and its Verifiers (graders). tasks/ticket-1/task.yaml:
name: ticket-1version: 0.1.0instructions: instruction.mdenvironment: support-queueverifiers: - correct-categoryinfo: ticket_id: ticket-1initial_state: ticket_id: ticket-1A Benchmark (the exam) lists its Tasks by name, with the rules for ranking results. It also needs a README.md beside benchmark.yaml, and unset scoring fields take their defaults:
name: support-triageversion: 1.0.0description: Categorize and resolve support tickets according to policy.purpose: Measures whether an Agent routes a ticket to the right team and closes it.tasks: - ticket-1 - ticket-2 - ticket-3scoring: description: Share of tickets resolved with the expected category and a drafted reply.A name that does not resolve is reported with the command that creates or restores it.
There is no Job file. Choose what to run with plural run, or construct a Job in Python. With three Tasks and one Agent, plural run --benchmark support-triage --agent careful --attempts 2 plans six Trials (two attempts at each Task).
Include Python behavior
An Environment (the world the Agent acts in) is a set of Apps. With one Python App, environment.yaml names the App class beside it and the Runtime it runs in:
name: support-queueversion: 1.0.0python: environment.py:SupportQueuereadme: README.mdruntime: provider: localWith several, or with an App that connects an existing system, list them under apps. Each entry is a Python App or a named mcp, openapi, or replay App with that kind's settings:
name: support-deskapps: - python: apps/queue.py:SupportQueue - name: crm mcp: url: https://sandbox.mcp.example.com/v1 headers: Authorization: Bearer ${CRM_TOKEN} credentials: [CRM_TOKEN]runtime: provider: localA Harness names its subclass and supplies the configuration it reads from self.config:
name: support-loopversion: 1.0.0python: harness.py:SupportHarnessconfig: max_turns: 12A deterministic Verifier names its function:
name: correct-categoryversion: 0.1.0kind: deterministiccheck: verify.py:checkPlural imports the referenced object and hashes the whole resource folder, so helper modules and data files beside it are part of the content hash. There is no command or source block to keep in sync.
Good to know: Thelocalruntime is a trusted subprocess, not a sandbox. Usedockerfor code you do not trust.
The Environments page lists every Environment manifest field.
Load manifests from Python
plural.project loads the same manifests the CLI reads, validates them with their dependencies, and returns ordinary SDK objects:
from plural import Jobfrom plural.project import Project, Workspace workspace = Workspace(Project.find())benchmark = workspace.get("benchmark", "support-triage")agent = workspace.get("agent", "careful")result = Job(benchmark, agents=[agent]).run()Project.find()walks up from the current folder to the nearestproject.yaml.workspace.gettakes a kind name or its CLI noun, soworkspace.get("env", "support-queue")also works.- An invalid resource raises
ProjectErrorlisting every problem. - A Job built this way uses the same resources and content hashes as the equivalent
plural run. It runs locally and is not recorded under.plural/jobs/, soplural job listdoes not show it.
Objects built directly in Python, such as a Task(...) in a script, run with Job as well. Manifests are how the CLI and hosted projects exchange them.
Inspect the resolved resource
To see exactly what Plural will use, after names are resolved and defaults filled in:
plural task validate ticket-1plural task show ticket-1 --jsonvalidate loads the resource and everything it depends on and prints its version and content hash. show --json adds its dependencies and whether it has been pushed ("hosted": "not pushed" until it is). The same verbs exist for env, verifier, harness, agent, and benchmark.