Skip to content

Beta: this feature may change before general availability

An Agent is a definition: a system prompt, the inference source it runs on and a cost cap. An AgentRun is one conversation with it. Each run works in its own sandbox, where the model runs shell commands to check its work, and it survives restarts of Nodus by replaying recorded results. An interrupted command or model call using your own key parks with NeedsResolution when its outcome is uncertain; inspect its effects before starting replacement work.

Agents, shared-sandbox subagents, parallel AgentGroups and Environment evaluation batches are in Beta.

  1. Open Agents → New agent to save your model, reusable instructions and model spending limit. Choose a model by name; use Enter a model or endpoint ID for a custom selection.
  2. Choose New run, select that agent, and describe the result you want. Leave subagent count on Auto or set a maximum. Start run submits the task.
  3. Open Team to see what is happening, which tasks are done, and whether anything needs attention. Search assignments or filter to active tasks. Pause updates freezes the live view without pausing the run.
  4. Read the Answer on Overview. The Team tab shows the parent’s review and the actual check results.

An agent is reusable configuration; a run is one task. Subagent count is optional: the parent chooses how many subagents it needs within your configured ceiling. Each run shares one sandbox among its parent and children. Enable Keep the sandbox for follow-up messages only when you need to continue the conversation; compute can continue accruing charges while it waits. Send the next task from the run’s Overview → Send follow-up. Cancel the run when finished.

In Agents → New agent, choose an inference source. The parent and its parallel subagents share this choice. Changes to an Agent apply to new runs; existing runs keep their admitted settings.

Source Select
Nodus On Demand A tool-capable model from On Demand, or endpoint/name
My hosted model Coming soon; currently unavailable
Bring my API key OpenAI-compatible or Anthropic API, model id and saved Secret
Managed Claude Automatic Claude routing

Named endpoints retain their access, concurrency and budget policies; an endpoint restricted to named API keys cannot be invoked through the agent’s service identity.

import nodus
agent = nodus.ModelAgent("researcher", model="openai/gpt-oss-20b", max_parallel_agents=8)
print(agent.remote("Use two parallel subagents to compare the approaches, then combine the findings."))

For your own key, save it as a project Secret, then select it:

agent = nodus.ModelAgent(
"researcher", protocol="OpenAI", model="gpt-4.1-mini", api_key_secret="model-key"
)

A single-value Secret can use any field name. With multiple values, use OPENAI_API_KEY, ANTHROPIC_API_KEY, or specify api_key_field. A compatible provider can set base_url="https://api.example.com/v1"; private addresses and redirects are refused. Keys are not placed in agent files or prompts. ChatGPT and Claude chat subscriptions have separate API billing; subscription sign-in is not supported here.

My hosted model is coming soon. The option is reserved; hosted Agent definitions and runs are rejected before any compute starts. Use On Demand or BYOK for now.

For On Demand, parallel calls split the remaining per_run_max_cost allowance before dispatch. BYOK calls have no Nodus model charge; their token usage is still shown. Set provider-side limits for BYOK. An uncertain BYOK call parks with NeedsResolution instead of repeating it.

agent.yaml
apiVersion: nodus.dev/v1
kind: Agent
metadata:
name: hello-agent
spec:
system: |
You are a careful assistant. Run shell commands in your sandbox to check your work, and answer in one short
paragraph.
# Nodus's Claude: each prompt is routed to Haiku, Sonnet, Opus or Fable, and billed from your credits.
model:
access: Nodus
perRunMaxCostUSD: "0.50"
Terminal window
nodus login
nodus apply -f agent.yaml

The agent uses Nodus’s Claude. Every prompt goes to a small router that picks the right Claude model for it (Haiku for simple requests up to Fable for demanding ones). That model answers the whole prompt, including its commands, so the model never changes in the middle of a turn. If the router is unavailable, Sonnet answers. Restrict the choice with spec.model.families, for example [haiku, sonnet].

run.yaml
apiVersion: nodus.dev/v1
kind: AgentRun
metadata:
name: hello-agent-run
spec:
agent: hello-agent
input: "Use the shell to print the Python version, then tell me which one it is."
Terminal window
nodus apply -f run.yaml
nodus wait agentrun/hello-agent-run --for=jsonpath='{.status.phase}'=Succeeded --timeout=8m
nodus get agentrun/hello-agent-run -o yaml

Or without a file: nodus create agentrun triage --agent hello-agent --prompt "Summarize the logs". To start from Nodus’s ready-made assistant, name the template claude-assistant.

status.turns lists each prompt with the model that served it, how it was chosen (jev or fallback) and its cost. status.answer.preview holds the first 4 KiB of the final answer; nodus agentrun answer prints all of it.

Terminal window
nodus agentrun answer hello-agent-run
nodus agentrun steps hello-agent-run

steps lists what the run did in order: creating its sandbox, each routing and model call, each command. A step that shows Completed is never repeated, even after a restart.

The console’s Agents page shows the same: each run with the model of its latest turn and its cost, and on a run every turn with the model that served it, how that model was chosen and what the turn cost, then the answer and the recorded steps. Overview keeps the original prompt visible as the run updates. Its execution-attempt count includes startup retries; it does not mean the agent recovered completed work.

import nodus
agent = nodus.ClaudeAgent("helper", system="You are careful.", families=["haiku", "sonnet"])
print(agent.remote("Use the shell to print the Python version")) # runs to the end, returns the answer
run = agent.submit("Summarize the logs", keep_alive=True) # a handle: run.answer(), run.steps(), run.cancel()
run.send("message", "Now the staging logs")

ClaudeAgent creates the agent on first use. Pass api_key_secret="anthropic-key" to use your own Anthropic key, or ClaudeAgent.from_name("claude-assistant", project="nodus") to run the ready-made template. To run many at once from Python, see Run agents in parallel.

Set spec.keepAlive: true and the run waits for messages after each turn instead of finishing. While it waits it uses no model and keeps its sandbox, which can still accrue compute charges. Set spec.deadline to bound the run’s lifetime. The deadline also applies while waiting for a message: the run fails with DeadlineExceeded and its sandbox is deleted without another model call.

Terminal window
nodus apply -f run.yaml # with keepAlive: true
nodus agentrun send hello-agent-run "Now do the same for the staging logs" --key staging-1

Sending the same --key again delivers the message once. A run that is still working reads the message after its current turn; a finished run refuses it with AgentRunFinished. Cancel a run with nodus cancel agentrun/NAME; its sandbox is deleted.

The same calls are REST: POST …/agentruns/NAME/messages with {"payload": "…", "messageKey": "…"}, and GET on …/steps and …/answer.

The parent agent can call parallel_agents to choose how many subagents a task needs, up to eight at a time by default, gather their results and finish a larger task. Ask it to split independent work and give each child a clear goal and its own output paths:

run.yaml
apiVersion: nodus.dev/v1
kind: AgentRun
metadata:
name: shared-subagents
spec:
agent: hello-agent
# Run at most two ready subtasks together; omit or use zero for Auto.
parallelism: 2
input: |
Use one parallel_agents batch with these named tasks in our shared sandbox:
1. primes: Compute the sum of the primes below 30 using Python. Write the number to /tmp/primes.txt.
2. squares: Compute the sum of the squares from 1 through 5 using Python. Write the number to /tmp/squares.txt.
3. total: After primes and squares complete, read their files, sum the numbers, and write /tmp/total.txt.
Declare each task's own output path in writes. total depends_on primes and squares; the first two run
concurrently. After the batch finishes, read all three files yourself, verify them,
call verify_team with a Python assertion for both numbers and their total, and reply with exactly: primes=129 squares=55 total=184

Use the hello-agent definition above, then apply this run:

Terminal window
nodus apply -f run.yaml
nodus agentrun steps shared-subagents
nodus agentrun answer shared-subagents

The parent chooses the smallest useful set of subagents for the task. Override its ceiling with spec.maxParallelAgents (from 1 to 32; default 8), and request an exact subagent count in the prompt when needed. For example:

Terminal window
nodus patch agent/hello-agent --patch '{"spec":{"maxParallelAgents":16}}'

The override applies to new runs. In Python, use nodus.ClaudeAgent("helper", max_parallel_agents=16). Increasing the ceiling does not increase the run’s budget or tool allowance: choose perRunMaxCostUSD and maxToolCalls to cover the larger task. New sandboxes reserve one vCPU and 1 GiB per eight allowed workers, rounded up (up to 4 vCPU and 4 GiB at 32). A smaller run maximum reduces this reservation. Existing sandboxes keep their original resources. Combined answer text is capped at 128 KiB; put longer results in shared files.

For each run, Auto chooses how many ready tasks work at once from task dependencies, assigned files, configured sandbox capacity and the remaining model allowance. The Team tab explains its selection. Choose Set a maximum to override the capacity heuristic; the Agent ceiling and spending/tool limits still apply. Set spec.parallelism: 6, use nodus create agentrun NAME --agent NAME --prompt "TASK" --parallelism 6, or call agent.submit("TASK", parallelism=6). Omit it or use zero for Auto. This does not resize an existing sandbox or guarantee a final price. BYOK provider spending is not measured or capped by Nodus.

The Team view and CLI report Peak parallel model calls from recorded invocation timings. This measures overlap among finished child calls, not the requested team size or queued assignments. Older calls without timings and unresolved calls are excluded, so the number is a lower bound. For a total cap covering compute and managed inference together, use a project-scoped Budget with period: Total and action: Block; see examples/agents/product-showcase/budget.yaml. Keep that budget across rehearsals so spend is cumulative.

Children share the parent’s sandbox and files. Each has its own conversation, so the parent supplies the context it needs in the task description. They use the same model as the parent and can run commands, read files and write files concurrently. The parent waits for the batch, reads the answers and shared files, and can delegate another batch for the next stage. For new runs, the parent gives each task a name, declares its output files or folders, and lists any earlier tasks it depends on. Independent tasks run together. Tasks with overlapping assigned paths wait for the current owner to finish; dependent tasks receive their prerequisites’ saved results.

For example, ask for two research tasks that write separate files, followed by a synthesis task that depends on both. You describe the outcome; the parent builds the task plan. Developers configuring a parent use this parallel_agents tool input:

{"tasks":[
{"name":"research","task":"Research the approach; write /tmp/research.md.","writes":["/tmp/research.md"]},
{"name":"summary","task":"Summarize the findings in /tmp/summary.md.","depends_on":["research"],"writes":["/tmp/summary.md"]}
]}

Task names are unique lowercase letters, digits or hyphens, beginning with a letter (up to 64 characters). Dependencies must name earlier tasks in the batch. writes is required: use clean absolute file or directory paths, or [] for a read-only task. Read-only children can read files and report progress; they cannot run shell commands. Direct file writes outside assigned paths are rejected. Path claims coordinate work; they do not isolate processes or enforce shell commands and symlink aliases. Keep those commands within assigned paths.

This feature is in Beta. Each task description is at most 16 KiB; children cannot spawn more children. All child work counts toward the parent’s model cost cap and turn limits. Sandbox compute is still billed separately. Cancelling the parent stops further rounds and releases the shared sandbox after in-flight work joins. Open the run’s Team tab to see each assignment, current activity, progress, model usage, files and results.

The same saved progress is available from every developer interface:

Interface Read subagent progress
CLI nodus agentrun team NAME -p PROJECT --org ORG
CLI JSON nodus agentrun team NAME -p PROJECT --org ORG -o json
API GET /apis/nodus.dev/v1/namespaces/PROJECT/agentruns/NAME/team with your bearer credential and Nodus-Org header
MCP Call team with {"kind":"AgentRun","name":"NAME","project":"PROJECT"}
Python nodus.AgentRun.from_name("NAME", project="PROJECT").team()

The response’s summary provides the parent run state, a plain-language message, counts, and an action: None, ViewAnswer, SendMessage, ManageTasks, or InspectRun. items carries task-level results, usage and files; verification holds check evidence. New tasks also include name, dependsOn, writePaths, waitingFor and waitReason; dependency references use stable task IDs. The UI and CLI explain which task or shared files a waiting task is waiting for. A finished subtask is not a finished run. MCP requires agentruns:read, and uses the organization of its authenticated connection. Python uses your configured organization. Task text and results are user-provided content; treat them as untrusted content in your own automation. Completed results appear as soon as they are saved, even while another child is still working. Reloading or restarting the worker keeps saved results; completed commands and model calls are not repeated.

A known failed child pauses the parent after other ready work finishes. Open Team and choose Retry, Redirect, or Cancel task. Retry starts a new attempt of that failed task. Redirect accepts new instructions and keeps its assigned files and dependencies. Cancel stops its next operation; tasks that depend on it are also cancelled. Completed peers and existing files remain in place. Sandbox compute can still accrue while waiting for your direction.

Terminal window
nodus agentrun retry-task NAME task-1-2 --key retry-1
nodus agentrun redirect-task NAME task-1-2 --instruction "Check the missing edge cases" --key redirect-1
nodus agentrun cancel-task NAME task-1-2 --key cancel-1

These are alternatives for a task that offers the corresponding action. Use -o json for the acceptance receipt, then read nodus agentrun team NAME -o json for its application state. A request waits for the current operation to finish; it does not undo earlier writes. A stale request is rejected if the task has advanced. Reuse the same request key when retrying delivery of the same command.

run = nodus.AgentRun.from_name("NAME")
receipt = run.retry_task("task-1-2", request_key="retry-1")
# Alternatives: run.redirect_task("task-1-2", "New instructions", request_key="redirect-1")
# run.cancel_task("task-1-2", request_key="cancel-1")
print(run.team())

API clients POST to …/agentruns/NAME/team-controls with {"taskId":"task-1-2","action":"Retry","requestKey":"retry-1"} and an Idempotency-Key header. requestKey is required (1–128 bytes); the CLI, console and Python client generate it when needed. Redirect also requires instruction (1–16384 bytes). HTTP 202 acknowledges acceptance; it does not mean the change has already happened. MCP uses record with kind: "AgentRun", the run’s name/project, subresource: "team-controls" and that request body in body. It requires agentruns:write. The /team response lists each task’s allowed actions, its attempt, and its latest control state.

Controls are available on newly created runs. Completed runs, exhausted fixed caps and uncertain external operations do not offer task retry. NeedsResolution still requires inspecting the unknown effect before starting replacement work. Retry does not reset spending or model/tool limits; the parent must verify the combined result again before completing.

File writes through write_file are attributed to the child. Shell changes appear as reported outputs when an agent calls report_progress; the view reads and fingerprints those files, but does not infer who changed arbitrary shared files. Assigned paths control which tasks may run together, while shell edits still rely on the task following its instructions.

The parent reviews the combined result and calls verify_team with relevant test commands before completing. The Team tab shows actual exit codes and check output. For non-code work it may record why tests do not apply; that appears as Reviewed, rather than Checks passed. Later edits invalidate the review. A run that exhausts its turn without current verification fails with TeamVerificationRequired instead of claiming success. Children leave a fifth of the model cap for parent review; increase the budget when the whole task needs more.

Read the same saved view with run.team() in Python or GET …/agentruns/NAME/team. The Steps tab includes task-BATCH-CHILD.assignment, .model and .tool.TOOL records, plus team.verification. Runs admitted before subagent tracking keep their original subagent.N history. Uncertain external operations still require inspection; a completed child is not rerun merely because another child needs resolution.

An AgentGroup runs many runs of one agent at once. You choose how many run at the same time and how much the whole group may spend. The other runs wait their turn, and one command cancels or deletes all of them. Create the agent and the group, then one run for each task:

agent.yaml
apiVersion: nodus.dev/v1
kind: Agent
metadata:
name: parallel-agents-worker
spec:
system: |
Follow the task's requested answer format; otherwise answer in one short sentence.
# Nodus's Claude, limited to Haiku so each run stays far below its cap.
model:
access: Nodus
families: [haiku]
maxTokens: 1024
perRunMaxCostUSD: "0.10"
group.yaml
apiVersion: nodus.dev/v1
kind: AgentGroup
metadata:
name: parallel-agents
spec:
agent: parallel-agents-worker
limits:
maxActive: 2 # at most two runs go at once
maxCostUSD: "0.60" # a run starts only while its own cap still fits under what is left
runs.yaml
apiVersion: nodus.dev/v1
kind: AgentRun
metadata:
name: parallel-agents-a
spec:
agent: parallel-agents-worker
group: parallel-agents
taskKey: a
input: "What is the capital of France?"
---
apiVersion: nodus.dev/v1
kind: AgentRun
metadata:
name: parallel-agents-b
spec:
agent: parallel-agents-worker
group: parallel-agents
taskKey: b
input: "What is the capital of Japan?"
---
apiVersion: nodus.dev/v1
kind: AgentRun
metadata:
name: parallel-agents-c
spec:
agent: parallel-agents-worker
group: parallel-agents
taskKey: c
dependsOn: [a, b] # c waits until a and b have succeeded
input: "Say in one sentence that both questions have been answered."
Terminal window
nodus apply -f agent.yaml
nodus apply -f group.yaml
nodus apply -f runs.yaml
nodus patch ag/parallel-agents --patch '{"spec":{"sealed":true}}'

Or without a file: nodus create agentgroup parallel-agents --agent parallel-agents-worker --max-active 2 --max-cost 0.60.

Each run is a task of the group: spec.group names the group and spec.taskKey names the task (a DNS label that is unique in the group). The run is named <group>-<taskKey>; leave metadata.name out or set exactly that. The run uses the group’s agent, so spec.agent must match it. The last command seals the group, which says that no more runs are coming (see below).

Terminal window
nodus wait ag/parallel-agents --for=jsonpath='{.status.phase}'=Succeeded --timeout=12m
nodus get ag
nodus get ar --field-selector spec.group=parallel-agents
nodus agentrun answer parallel-agents-c

nodus get ag lists each group with its phase, RUNS (finished successfully out of all) and ACTIVE runs, and its cost. status.counts splits the runs into queued, active, waiting (for a message or for funds), succeeded, failed and cancelled. status.blockedReasons says why queued runs have not started and how many wait for each reason: DependencyWait, MaxActiveReached or MaxCostReached. A queued run’s own status.reason is DependencyWait, or GroupWait while it waits for a free slot or for room under the cost cap. The runs you list are ordinary runs, so status.answer, nodus agentrun answer and nodus agentrun steps work on each of them.

A group is Running until it is sealed and every run has finished. It then becomes Succeeded, or Failed with reason RunsFailed when a run failed or was cancelled because a dependency failed. A sealed group with no runs succeeds.

spec.limits.maxActive is how many runs the group lets run at the same time (default 10, 1 to 1000). Change it on a live group:

Terminal window
nodus patch ag/parallel-agents --patch '{"spec":{"limits":{"maxActive":4}}}'

spec.limits.maxPending is how many unfinished runs the group takes in (default 10000); a run beyond it is refused.

spec.dependsOn lists the task keys a run waits for. It starts only after every one of them has succeeded, and until then it is queued with reason DependencyWait. A dependency must already be in the group, so create the runs it depends on first (earlier in the same file works). When a dependency fails, the run is cancelled with reason DependencyFailed; when a dependency was cancelled, the run is cancelled with reason DependencyCancelled.

spec.maxCostUSD is one cap for the whole group. A run starts only when its own cap (the agent’s perRunMaxCostUSD) still fits under what is left: the cap minus what the group has spent and minus the caps of the runs that have started and not finished. The members together can therefore never spend more than the cap, and runs that do not fit yet wait with MaxCostReached. In the example, the $0.60 cap has room for the caps of six runs at $0.10 each before any run has spent anything. Raise the cap on a live group with nodus patch; it cannot be lowered. If the cap is smaller than one run’s cap, no run starts: raise it or lower the agent’s perRunMaxCostUSD. status.cost shows totalUSD and limitUSD. The cap covers the runs’ Claude usage (model and routing calls), the spend each run’s perRunMaxCostUSD limits; the sandbox each run works in is billed on its own and is not counted against either cap.

A group takes new runs until you seal it with spec.sealed: true. Sealing cannot be undone, and a sealed group refuses new runs with a conflict. A group finishes only once it is sealed, so seal it after you create the last run.

Terminal window
nodus cancel ag/parallel-agents
nodus delete ag/parallel-agents

nodus cancel cancels every run that has not finished and leaves the finished runs, with their answers, as they are. status.phase goes through Cancelling to Cancelled. nodus delete removes the group together with all of its runs.

Create an evaluation group with an Environment at an explicit version. Nodus generates the fixed task batch, runs it through ordinary AgentRuns, and grades completed answers using the Environment’s isolated grader. There is no manual submit or seal step. Set a project Budget to limit total spend: the group cap covers model usage, while agent and grading Sandbox compute is billed separately.

After deploying the example’s parallel-agents-worker Agent above, run this Python example. It creates two held-out tasks, waits for a terminal result, prints per-case outcomes and deletes the evaluation group afterward.

evaluation.py
"""Evaluate the deployed example agent on a fixed, versioned task batch."""
import nodus
def main() -> None:
# Model caps exclude Sandbox compute; set a project Budget before running this example.
evaluation = nodus.AgentGroup.create(
"parallel-agents-eval",
agent="parallel-agents-worker",
max_active=2,
max_cost="0.20",
evaluation={
"environment": "nodus/[email protected]",
"split": "test",
"tasks": 2,
"repetitions": 1,
"seed": 42,
"timeout": "10m",
},
)
try:
status = evaluation.wait(timeout=660)
results = evaluation.results()
print(
f"{status.phase}: cases={len(results)}, pending={status.evaluation.pending}"
)
print("pass rate:", status.evaluation.get("passRate"))
print("mean reward:", status.evaluation.get("meanReward"))
for result in results:
print(result.task_id, result.state, result.get("verdict"))
finally:
evaluation.delete()
if __name__ == "__main__":
main()

The batch is at most 100 cases including repetitions. Every member pins the same Agent revision; results record the Environment version, resolved image digest, split and seed. Only task prompts reach the agent, never hidden answers. The timeout defaults to 30 minutes, may be 1 minute to 24 hours, and includes manifest preparation, queueing and funding waits from group creation. Each member receives that absolute deadline. Grading also stops at the deadline; completed answers that could not be scored are shown as evaluation timeouts, excluded from pass rate and mean reward.

Open the group in the console to see scored cases, pass rate, mean reward, pending cases and execution/grading failures separately. The Results view links each case to its AgentRun and shows grader evidence. A failed run, missing answer or grader failure is unscored and does not become a zero reward. A wrong or invalid answer is a scored result. Per-case records share the member journal’s 30-day payload retention; deleting the group removes its runs and records. Canceling or finishing the group cleans up its grading pool.

Store the key in a Secret under the key ANTHROPIC_API_KEY and point the agent at it. Nodus then charges nothing for the model, and your Anthropic account pays for it.

spec:
model:
access: BYOK
name: claude-sonnet-5-5
apiKeySecret: anthropic-key

spec.perRunMaxCostUSD (default $1.00) caps what an On Demand or managed Claude run spends on the model. Before each call the run checks that it fits under the cap. When it does not, the run waits (reason: MaxCostReached) and makes no more calls. The cap is copied from the agent when the run is created, so changing the agent does not change a run that has started: to continue the work, cancel the run and start a new one. If your credits run out, the run waits instead and continues when you add credits. BYOK inference uses your provider’s spending limits. Sandbox compute is billed separately and is outside this model cap. status.cost shows the model spend so far. Runs in a group also share the group’s cap (see Run agents in parallel).

  • One run works one prompt at a time, with at most spec.maxToolCalls commands per turn (default 50).
  • The input and each message are at most 256 KiB. A run’s recorded payloads are kept 30 days after it ends.
  • A run created for a stopped agent (spec.state: Stopped) is refused with AgentStopped.
  • A group takes at most 10000 unfinished runs. Create runs in batches of up to 100 by posting an AgentRunList to …/agentruns with an Idempotency-Key header; a batch is all-or-nothing, and its runs are created in list order, so list each run after the runs it depends on.

See Durable execution for what survives a restart.

In the console, Agents has one row of views: Sandboxes, Runs, Groups, and Definitions. Use the page’s ⋯ menu to copy its CLI command.

Agents accept the same env, secrets, and network settings as Sandboxes. Use project Secrets for GitHub deploy keys and other tool credentials. Parent and child tools share these permissions, so scope credentials to the repositories and services the task needs. Keep secrets out of prompts and command output. The default outbound policy is Deny; explicitly select network.egress.policy: AllowList with allow: [github.com, api.github.com] for GitHub HTTPS access. Add the domains the task needs. Tools must honor the injected HTTPS_PROXY/HTTP_PROXY settings. Open permits public Internet access; private and metadata addresses stay blocked. These settings are pinned when a run is admitted, and secret delivery uses the Sandbox path.