Migrate to 0.15
If you used Plural before 0.15, this page tells you what moved and walks you through updating your project. Your Environments, Tasks, Verifiers, and Agents keep working the same way. What changed is where their files live and which commands you type.
Plural 0.15 organizes everything around a project directory and one command shape. Each resource lives in its own directory and is addressed by kind and name, as in plural task validate refund. The SDK objects you author (Environment, Task, the Verifiers, Harness, Agent, Benchmark, and Job) are unchanged. What changed is how files are laid out, how the CLI finds them, and how they reach a hosted project.
Tip: to jump straight to the steps, see Migrate a project.
Breaking changes
Project layout
A project is a directory that contains project.yaml. Commands walk up from the current directory to find it, so they work from any subdirectory.
project.yaml name and descriptionplural.lock hosted revisions, updated by push and pull; commit itpyproject.toml dependencies = ["plural>=0.18.0"].plural/ local state: hosted binding, Jobs, backups (gitignored)environments/<name>/ environment.yaml, server.py or environment.py, README.mdtasks/<name>/ task.yaml, instruction.mdverifiers/<name>/ verifier.yaml, verify.pyharnesses/<name>/ harness.yaml, harness.pyagents/<name>/ agent.yamlbenchmarks/<name>/ benchmark.yaml, README.mdA resource is named by its directory, and its manifest's name must match. Manifests refer to one another by name, for example environment: support-queue in a task.yaml. Single-file job.yaml and job.py projects are no longer read.
Commands
| Before | Now |
|---|---|
plural init <dir> | plural project init <name> |
plural validate <file> | plural <kind> validate <name> |
plural inspect, plural export | plural <kind> show <name> --json |
plural run job.yaml | plural run -b <benchmark> -a <agent> |
plural run <file> --agent <file:attr> | plural run -t <task> -m <model> or -a <agent> |
plural <kind> publish | removed; push makes a revision available |
plural job submit, plural job watch | plural run --record; every Job runs from your machine |
plural trial list, plural trial watch | plural job show <job>, plural trial show <trial> --follow |
plural benchmarks show|export | plural benchmark show [--json]; benchmarks diff is removed |
plural review hosted-list|hosted-submit | plural review list|submit |
plural models show | plural model list [--provider] |
plural schemas, plural harness schema | plural.project.manifest_schemas() in Python, or download project-schemas.json |
plural login, plural logout, plural status | plural auth login, plural auth logout, plural auth status |
Each resource kind (env, task, verifier, harness, agent, benchmark) has the same verbs: init, validate, push, pull, show, and list. Omitting the name uses the resource directory you are in.
Removed plural run options: --mode, --per-runtime-concurrency, --watch and --no-watch (use --follow), --idempotency-key, --api-key (export PLURAL_API_KEY instead), --name, --format, and --catalog. An Agent is named with -a, or given inline with -m and an optional -h. Without -h the Agent uses native, Plural's built-in tool loop.
Every run is a Job
plural run always creates a Job. A local Job is recorded under .plural/jobs/<job-id>/ with the version and content hash of every input it used. plural job rerun and plural trial rerun run those pinned inputs again as a new Job linked to the original, even if the files on disk have changed since. --record also records the Job in the hosted project, and first pushes any input the hosted project does not hold yet, as the resource's next numbered revision.
Revisions are no longer published
A push creates an immutable revision that is immediately available inside its project. There is no publish step and no "published" status; hosted revisions that were published before the upgrade are now reported as available.
Pushing never makes anything public. Listing a Benchmark on the Hub, or publishing its results, is a separate action in the Plural web app.
Pushes are duplicate-free: unchanged content reuses the existing revision, and changed content becomes the next numbered revision. You never bump version; since 0.18 it is an optional release label. Without --with-deps, every dependency must already be pushed with identical content. Nothing is uploaded unless the whole push can succeed.
A push now uploads the resource's whole directory with the revision, and the revision stays private to its project. Both the CLI and the service refuse files that look like credentials (such as .env and private keys) and paths that point outside the resource directory; list anything else you want to leave out in the resource's .pluralignore. plural <kind> pull restores a revision's files. It refuses to overwrite local files that differ unless you pass --force, which keeps the old copy under .plural/backups/.
Authentication and scope
plural login,logout, andstatusare nowplural auth login,auth logout, andauth status. Credentials are still stored in your user config directory or OS keyring, never in the project.plural auth scopechooses where hosted commands go: your account (.or--account) or one hosted project (-p <name>). The new scope is checked with the service before it is saved; a failed check keeps the previous scope. Scope selects a destination only. It never changes what your credential is allowed to do.plural project init <name> --pushcreates the hosted project privately, or connects to an existing one, records the binding in.plural/project.json, and selects project scope.- A push is refused when your scope names a different project than the one the checkout is bound to.
- An API key limited to one project can only list, read, and write that project, and cannot create projects.
Models
plural model list shows the models your organization allows. Organization admins can restrict that list; runs, reruns, and gateway calls enforce it on the server.
Python API
| Before | Now |
|---|---|
plural.project.Resolver(root=...).load("job.yaml") | Workspace(Project.find()).get("benchmark", "<name>") |
plural.load, plural.dump, plural.dumps | manifests in the standard layout, loaded with Workspace |
plural.cli.scaffold.load_harness(path) | plural.project.resources.load_harness_directory(path) |
Studio.<kind>.publish(...) | removed; Studio.<kind>.push(...) returns an available revision |
Job(source, agents=[agent]).run() still runs locally and is not recorded under .plural/jobs, so plural job list does not show it. Job(..., client=Client()) still sends model calls through the Plural gateway, using your Plural API key from PLURAL_API_KEY or the key stored with plural auth login --api-key-stdin.
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()Renamed fields and classes
These names changed so that one word means one thing across the SDK. The old names still work for one release: an old key in a YAML file or a saved revision loads, and an old import or attribute returns the same object, each with a DeprecationWarning naming the new one. Everything is saved under the new name.
| Before | Now |
|---|---|
Agent harness_kwargs | harness_config |
Agent auth_mode | auth |
Agent secret_names | secrets |
Harness and HarnessDefinition environment (variable names) | env_vars |
Runtime network_allowlist | allowed_hosts |
Runtime build_timeout_sec | build_timeout_seconds |
Runtime and VerifierRuntime resources (CPU and memory) | compute |
plural.Plural | plural.Client |
plural.TimeoutError | plural.errors.PluralTimeoutError |
plural.Capability | plural.sandbox.SandboxCapability |
plural.Outcome | plural.tracing.TraceOutcome |
plural.Trial, plural.execution.Trial | plural.execution.TrialRunner, internal to Job |
Renaming a field changes no content hash, so saved Agents, Environments, and Verifiers keep their identities. TrialResult now also reports the episode's stop_reason, step_reward_total, and turns.
Migrate a project
Follow these steps in order. Validation in step 5 reports anything you missed.
- Upgrade:
python -m pip install --upgrade "plural>=0.18.0", oruv add "plural>=0.18.0"in a uv-managed project. - Create the project:
plural project init <name>makes./<name>with the standard files. Run the remaining commands inside it. - For each resource, run
plural <kind> init <name>and move its definition into the generated directory. Until you fill a template in,validatereports what is still missing. A new Environment's template uses thedockerruntime; setruntime.providerto what your old project used. Python behavior, such as an Environment class or a Verifier'scheckandrewardfunctions, stays Python and is referenced from the manifest, for examplepython: environment.py:SupportQueueorcheck: verify.py:check. - Replace object references with names: a Task lists
environment: <name>andverifiers: [<name>], and a Benchmark liststasks: [<name>, ...]. - Check everything with
plural benchmark validate <name>. It validates the Benchmark and everything it depends on, and reports every problem at once. - Run it:
plural run -b <benchmark> -a <agent> --dry-run, then without--dry-run. - Delete
job.yaml,job.py, and any script that calledResolver,dump, orpublish. - To use a hosted project, run
plural auth login, thenplural project init <name> --push(with the name fromproject.yaml) andplural benchmark push <name> --with-deps. Commitplural.lock.
The support queue and Wordle tutorials are complete projects in this layout.