Plural
Docs

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.

text
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.md

A 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

BeforeNow
plural init <dir>plural project init <name>
plural validate <file>plural <kind> validate <name>
plural inspect, plural exportplural <kind> show <name> --json
plural run job.yamlplural run -b <benchmark> -a <agent>
plural run <file> --agent <file:attr>plural run -t <task> -m <model> or -a <agent>
plural <kind> publishremoved; push makes a revision available
plural job submit, plural job watchplural run --record; every Job runs from your machine
plural trial list, plural trial watchplural job show <job>, plural trial show <trial> --follow
plural benchmarks show|exportplural benchmark show [--json]; benchmarks diff is removed
plural review hosted-list|hosted-submitplural review list|submit
plural models showplural model list [--provider]
plural schemas, plural harness schemaplural.project.manifest_schemas() in Python, or download project-schemas.json
plural login, plural logout, plural statusplural 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, and status are now plural auth login, auth logout, and auth status. Credentials are still stored in your user config directory or OS keyring, never in the project.
  • plural auth scope chooses 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> --push creates 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

BeforeNow
plural.project.Resolver(root=...).load("job.yaml")Workspace(Project.find()).get("benchmark", "<name>")
plural.load, plural.dump, plural.dumpsmanifests 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.

python
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.

BeforeNow
Agent harness_kwargsharness_config
Agent auth_modeauth
Agent secret_namessecrets
Harness and HarnessDefinition environment (variable names)env_vars
Runtime network_allowlistallowed_hosts
Runtime build_timeout_secbuild_timeout_seconds
Runtime and VerifierRuntime resources (CPU and memory)compute
plural.Pluralplural.Client
plural.TimeoutErrorplural.errors.PluralTimeoutError
plural.Capabilityplural.sandbox.SandboxCapability
plural.Outcomeplural.tracing.TraceOutcome
plural.Trial, plural.execution.Trialplural.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.

  1. Upgrade: python -m pip install --upgrade "plural>=0.18.0", or uv add "plural>=0.18.0" in a uv-managed project.
  2. Create the project: plural project init <name> makes ./<name> with the standard files. Run the remaining commands inside it.
  3. For each resource, run plural <kind> init <name> and move its definition into the generated directory. Until you fill a template in, validate reports what is still missing. A new Environment's template uses the docker runtime; set runtime.provider to what your old project used. Python behavior, such as an Environment class or a Verifier's check and reward functions, stays Python and is referenced from the manifest, for example python: environment.py:SupportQueue or check: verify.py:check.
  4. Replace object references with names: a Task lists environment: <name> and verifiers: [<name>], and a Benchmark lists tasks: [<name>, ...].
  5. Check everything with plural benchmark validate <name>. It validates the Benchmark and everything it depends on, and reports every problem at once.
  6. Run it: plural run -b <benchmark> -a <agent> --dry-run, then without --dry-run.
  7. Delete job.yaml, job.py, and any script that called Resolver, dump, or publish.
  8. To use a hosted project, run plural auth login, then plural project init <name> --push (with the name from project.yaml) and plural benchmark push <name> --with-deps. Commit plural.lock.

The support queue and Wordle tutorials are complete projects in this layout.