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> or ws-<timestamp> so you can identify and clean up stale identities in bulk with ravi 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