Multi-Agent Setup
Why separate Identities?
When running multiple agents, each should have its own Identity:
- Isolation — one agent’s credentials don’t leak to another
- Clean inboxes — each agent only sees its own messages
- Parallel operation — agents work independently without blocking each other
- Revocability — disable one agent without affecting others
Example setup
research-agent → research@in.ravi.app (newsletters, reports, data platforms)
scheduler-agent → calendar@in.ravi.app (meeting invites, reminders)
finance-agent → invoices@in.ravi.app (receipts, billing, expense tracking)
Create Identities
ravi identity create --name "research-agent" --json
ravi identity create --name "scheduler-agent" --json
ravi identity create --name "finance-agent" --json
Per-project configuration
Use per-project config files to automatically scope each project to its Identity. Place a .ravi/config.json in each project directory:
~/projects/research-agent/.ravi/config.json
{
"identity_uuid": "research-uuid-here",
"identity_name": "research-agent"
}
~/projects/finance-agent/.ravi/config.json
{
"identity_uuid": "finance-uuid-here",
"identity_name": "finance-agent"
}
When the agent runs from its project directory, the CLI automatically uses the correct Identity.
Dynamic workspace provisioning
The per-project config approach works well for a fixed set of named agents. For orchestrators that spin up workspaces on demand (amux, vibe-kanban, Claude Squad, lalph, Symphony, and similar), you need a different pattern: create an Identity at dispatch time and scope it to the workspace without touching global state.
Use RAVI_CONFIG_DIR — it overrides the config directory for a single process (or process tree) without writing to the global ~/.ravi/config.json.
# At workspace creation time:
IDENTITY=$(ravi identity create --name "task-${TASK_ID}" --json)
IDENTITY_UUID=$(echo "$IDENTITY" | jq -r '.uuid')
mkdir -p /workspace/.ravi
echo "{\"identity_uuid\": \"$IDENTITY_UUID\"}" > /workspace/.ravi/config.json
# All subsequent ravi calls in this process tree use the workspace identity
export RAVI_CONFIG_DIR=/workspace/.ravi
RAVI_EMAIL=$(ravi get email --json | jq -r '.email')
RAVI_PHONE=$(ravi get phone --json | jq -r '.phone_number')
Parallel safety: Any number of workspaces can use this pattern simultaneously. Each writes its own
.ravi/config.json; none writes to the global config.
Fleet initialization — launching N agents simultaneously
For tools that run 10–50+ agents in parallel (Supacode, dmux, cmux, vibe-tree), you need to provision all identities up-front and bind each to an isolated worktree before any agent process starts. This avoids the serialization penalty of creating identities mid-task and ensures zero shared state across the fleet.
import subprocess, os, pathlib, concurrent.futures, json, tempfile
def create_agent_identity(name: str, config_dir: pathlib.Path) -> dict:
"""Create a Ravi identity scoped to an isolated config directory."""
config_dir.mkdir(parents=True, exist_ok=True)
result = subprocess.run(
["ravi", "identity", "create", "--name", name, "--json"],
env={**os.environ, "RAVI_CONFIG_DIR": str(config_dir)},
capture_output=True, text=True, check=True,
)
identity = json.loads(result.stdout)
# Write config so every subsequent ravi call in this dir is scoped correctly
(config_dir / "config.json").write_text(
json.dumps({"identity_uuid": identity["uuid"]})
)
email_out = subprocess.run(
["ravi", "get", "email", "--json"],
env={**os.environ, "RAVI_CONFIG_DIR": str(config_dir)},
capture_output=True, text=True, check=True,
)
return {
"name": name,
"uuid": identity["uuid"],
"email": json.loads(email_out.stdout)["email"],
"config_dir": str(config_dir),
}
def initialize_fleet(worktrees: list[str]) -> list[dict]:
"""
Provision one Ravi identity per worktree, in parallel.
Safe: each identity uses its own RAVI_CONFIG_DIR — no global state is written.
"""
tasks = [
(f"agent-{i}", pathlib.Path(wt) / ".ravi")
for i, wt in enumerate(worktrees)
]
with concurrent.futures.ThreadPoolExecutor(max_workers=min(len(tasks), 20)) as pool:
futures = {
pool.submit(create_agent_identity, name, config_dir): name
for name, config_dir in tasks
}
fleet = []
for future in concurrent.futures.as_completed(futures):
fleet.append(future.result())
return fleet
# Usage — e.g. with 10 git worktrees
worktrees = [f"/workspace/feature-{i}" for i in range(10)]
fleet = initialize_fleet(worktrees)
for agent in fleet:
print(f"{agent['name']} → {agent['email']} (config: {agent['config_dir']})")
# agent-0 → agent-0@in.ravi.app (config: /workspace/feature-0/.ravi)
# agent-1 → agent-1@in.ravi.app (config: /workspace/feature-1/.ravi)
# ...
To then run a task in a specific agent’s context, pass RAVI_CONFIG_DIR to the subprocess:
import subprocess, os
def run_agent_task(worktree: str, command: list[str]) -> str:
config_dir = os.path.join(worktree, ".ravi")
result = subprocess.run(
command,
env={**os.environ, "RAVI_CONFIG_DIR": config_dir},
capture_output=True, text=True, check=True,
cwd=worktree,
)
return result.stdout
Teardown: when a worktree is removed, delete the identity to avoid accumulating stale identities:
ravi identity delete <uuid>.
Example: amux workspace setup hook
In .amux/workspaces.json, the setup array runs once per new workspace:
{
"setup": [
"IDENTITY=$(ravi identity create --name \"agent-$(basename $PWD)\" --json)",
"mkdir -p .ravi && echo \"{\\\"identity_uuid\\\": \\\"$(echo $IDENTITY | jq -r .uuid)\\\"}\" > .ravi/config.json",
"ravi secrets export > .env.local"
]
}
After setup, every ravi call from within that workspace directory automatically uses the scoped Identity.
Lifecycle: cleaning up dynamic identities
When a workspace is torn down, you can delete the config directory. If you want to fully remove the Identity from Ravi:
# Read the UUID from the workspace config before deleting it
IDENTITY_UUID=$(jq -r '.identity_uuid' /workspace/.ravi/config.json)
# Delete workspace config
rm -rf /workspace/.ravi
# Archive or delete the identity
ravi identity delete "$IDENTITY_UUID" --json
Use a descriptive naming convention like
task-<id>orws-<timestamp>so you can identify and clean up stale identities in bulk withravi identity list --json.
For a complete end-to-end harness example in bash, Python, and TypeScript, see Harness Integration.
Switching Identities manually
If you’re not using per-project configs, switch with:
ravi identity use research-agent
# ... run research commands ...
ravi identity use finance-agent
# ... run finance commands ...
OpenClaw multi-agent
For OpenClaw, configure each agent instance with a different Identity in the plugin config:
plugins:
ravi:
identityUuid: "research-uuid-here"
Each OpenClaw instance reads its own config, so agents stay isolated.
Security considerations
- Each Identity has its own encryption keys derived from the same PIN
- One Identity’s vault cannot be accessed from another Identity
- Revoking an Identity disables all its email, phone, and vault access
- Agent compromises are contained to the affected Identity
Next steps
- Identities — Identity management reference
- Harness Integration — complete dispatch-time identity injection guide (bash, Python, TypeScript)
- Production Patterns — Python and TypeScript patterns for production agent systems
- Security Model — how isolation works