Agents
ModelRelay supports agentic tool loops in three ways. Use the right one depending on reuse, coordination complexity, and governance needs.
Three approaches
| Approach | Best for | Notes |
|---|---|---|
llm.responses with tool_execution: "agentic" |
One-off logic scoped to a workflow | Self-contained; define tools + system prompt inline |
agent.run |
Reusable agents shared across workflows | Project-scoped agent resources with versions + auditability |
agent.scheduler |
Multi-agent coordination with iteration | Bounded loops, message-based coordination, coordinator-driven |
When to use which
Use llm.responses + agentic when:
- The behavior is specific to a single workflow
- You want everything defined inline
- You don’t need agent versioning
Use agent.run when:
- The agent is reused across workflows
- You need versioning and audit trails
- A team manages agents as shared resources
Use agent.scheduler when:
- Multiple agents need to coordinate iteratively
- A coordinator should decide when work is complete
- You need bounded multi-round loops (not just fan-out/join)
Examples
Inline agentic node
{
"id": "analyze",
"type": "llm.responses",
"tool_execution": "agentic",
"max_tool_steps": 6,
"tools": ["search", "summarize"],
"input": [{ "role": "user", "content": "Analyze Q4 results" }]
}
Project agent in a workflow
{
"id": "research",
"type": "agent.run",
"agent": "researcher@2",
"input": [{ "role": "user", "content": { "$ref": "#/input/query" } }]
}
Run an agent directly
POST /projects/{project_id}/agents/{slug}/run
{
"input": [{ "role": "user", "content": "Analyze Q4 results" }],
"options": { "max_steps": 10 }
}
Multi-agent scheduler in a workflow
{
"id": "code_review",
"type": "agent.scheduler",
"agents": [
{"agent": "coder@1", "count": 2, "input": [{"role": "user", "content": "{{task}}"}]},
{"agent": "reviewer@1", "count": 1, "input": [{"role": "user", "content": "Review code"}]}
],
"coordinator": {
"agent": "lead@1",
"input": [{"role": "user", "content": "Coordinate until review passes"}]
},
"policy": {
"max_rounds": 5,
"max_concurrency": 3,
"max_steps": 50,
"timeout_ms": 120000
}
}
The scheduler spawns participant agents, collects their messages, and runs a coordinator to decide next steps. See Agent Scheduler for details.
Agent memory
Agents have a persistent memory document that is injected into the system prompt on each run. This allows agents to retain information across runs—user preferences, learned facts, or operational notes.
How memory works
-
Automatic injection: When an agent run starts, the stored memory is prepended to the system prompt under a
## Agent Memoryheading, separated from the original system prompt by a horizontal rule. -
Full replacement: The
memory_writetool completely replaces the memory content. There is no append or merge—design your agent to rewrite the full document when updating. -
Size limit: Memory is capped at 32KB. Writes exceeding this limit return an error.
The memory_write tool
The memory_write tool is automatically available in agent runs—you don’t need to add it to your tools list. Calling it outside an agent context returns an error.
{
"type": "function",
"function": {
"name": "memory_write",
"description": "Update the agent's persistent memory",
"parameters": {
"type": "object",
"properties": { "content": { "type": "string" } },
"required": ["content"]
}
}
}
Example: learning user preferences
System: You are a helpful assistant. Use memory_write to remember user preferences.
User: I prefer dark mode and metric units.
Agent: [calls memory_write with content: "User preferences:\n- Theme: dark mode\n- Units: metric"]
--- Next run ---
System:
## Agent Memory
User preferences:
- Theme: dark mode
- Units: metric
---
You are a helpful assistant. Use memory_write to remember user preferences.
Messaging
Agents can communicate with each other using built-in messaging tools. This enables coordination patterns like delegation, request/response, and event notification.
Message tools
| Tool | Description |
|---|---|
message.send |
Send a message to an agent or run |
message.inbox |
Check for new messages (marks them as read) |
message.reply |
Reply to a message in a thread |
Addresses
Messages are sent to addresses that identify mailboxes:
run:<run_id>- A specific run’s mailboxagent:<slug>@<project_id>- An agent’s persistent mailbox
Example: delegation pattern
A coordinator agent can delegate work to specialists:
System: You coordinate data analysis. Use message.send to delegate to worker agents.
User: Analyze the Q4 sales data
Agent: [calls message.send with:
to: "agent:analyst@<project_id>"
subject: "Analyze Q4 sales"
body: {"dataset": "q4_sales", "metrics": ["revenue", "growth"]}
]
Agent: [calls message.inbox to check for replies]
Agent: [receives reply with analysis results]
Agent: Based on the analysis from our specialist...
Threading
Use thread_id to group related messages into conversations. When you call message.reply, it automatically continues the thread.
For full API details, see the Messages API reference.
Workflows vs Messaging
Workflows and messaging serve different coordination patterns. Use the right tool for your use case.
Workflows: Synchronous DAG
flowchart LR
A[Agent A] --> B[Agent B] --> C[Agent C]
Workflows execute agents in a predetermined, synchronous flow:
- Blocking: Agent A calls B, waits for the result, then C runs
- No cycles: DAG structure means no iteration or loops
- Single execution: Everything happens in one workflow run
- Predetermined: You define the exact call sequence upfront
Messaging: Asynchronous Coordination
flowchart TB
C[Coordinator] <-->|messages| W1[Worker 1]
C <-->|messages| W2[Worker 2]
C <-->|messages| W3[Worker 3]
Messaging enables dynamic, asynchronous coordination:
- Non-blocking: Send a message, continue working, check inbox later
- Enables iteration: Coordinator can do multiple rounds of back-and-forth
- Cross-run: Agent mailboxes persist beyond individual workflow runs
- Dynamic: Don’t need to know responses upfront
Decision matrix
| Scenario | Workflow | Scheduler | Messaging |
|---|---|---|---|
| Fixed pipeline (A→B→C) | ✅ | Overkill | Overkill |
| Fan-out, wait for all results | ✅ map.fanout |
Works too | Works too |
| Multi-round negotiation | ❌ No cycles | ✅ | ✅ |
| “Keep iterating until done” | ❌ | ✅ Bounded | ✅ |
| Coordinator decides routing | ❌ | ✅ | Manual |
| Bounded multi-agent loops | ❌ | ✅ Policy limits | DIY |
| Agents spawning other agents | ❌ | ✅ | ✅ |
| Long-running coordination | ❌ Single run | ❌ Single run | ✅ Persistent |
| Audit trail per step | ✅ Run events | ✅ Scheduler events | ✅ Message log |
Example: Choosing the right approach
Workflow works well:
User query → Research Agent → Summarize Agent → Response
Each step runs once, in order. Use a workflow.
Scheduler fits well:
User query → Scheduler
├─→ Coder 1 (implements)
├─→ Coder 2 (implements)
└─→ Reviewer (reviews)
↓
Coordinator (decides if done)
... bounded iterations ...
← Final response
Multiple agents iterate with a coordinator deciding completion. Use agent.scheduler.
Messaging needed:
User query → Coordinator
├─→ Researcher (may need clarification)
├─→ Fact Checker (may dispute findings)
└─→ Editor (may request revisions)
... unbounded, cross-run coordination ...
← Final response
Long-running coordination across multiple workflow runs. Use messaging directly.
Debugging agent runs
- Use
/runs/{run_id}/stepsfor per-step logs and tool I/O. - Use
POST /projects/{project_id}/agents/{slug}/testwithmock_toolsto replay tool calls.