Janus

Documentation

Quick Start

Install, configure, and run Janus, then complete your first agent-to-agent task handoff.

Prerequisites

  • Docker & Docker Compose (recommended path)
  • Python 3.10+ (for the SDK example)
  • Or, to build from source: Go 1.21+, PostgreSQL 13+, NATS 2.10+ (JetStream), Redis 7+

1. Get the Janus Source

The Janus repository ships with a production-ready Dockerfile and docker-compose.yml. Clone it to get everything you need:

git clone https://github.com/agentium-lab/Janus.git
cd Janus

The clone gives you:

  • Dockerfile — multi-stage build (Go builder → minimal Alpine runtime), produces the janus-api binary
  • docker-compose.yml — one command brings up Janus + PostgreSQL + NATS + Redis
  • configs/janus.example.yaml — reference configuration file
  • migrations/ — PostgreSQL schema migrations (auto-applied on boot)

2. Start Janus with Docker Compose

From the repo root, a single command builds the Janus image and starts all four services (janus-api, postgres:16, nats:2 with JetStream, redis:7):

docker compose up -d --build

The first build takes a couple of minutes (compiles the Go binary). Subsequent starts are instant. Verify every service is healthy:

curl -s http://localhost:8080/healthz
# → {"status":"ok"}

curl -s http://localhost:8080/readyz
# → {"status":"ready"}  (checks PG + NATS + Redis connectivity)

Janus exposes two ports:

  • 8080 — HTTP REST API & protocol gateways
  • 9090 — gRPC API

3. Build from Source (optional)

To run Janus outside Docker (development, custom builds, debugging):

# Start the three dependencies
docker compose -f deployments/smoke-deps.compose.yaml up -d postgres nats redis

# Build the server binary
cd server && go build -o janus-api ./cmd/janus-api/

# Run with environment variables
JANUS_PG_HOST=localhost JANUS_PG_USER=janus JANUS_PG_DATABASE=janus \
JANUS_NATS_URL=nats://localhost:4222 JANUS_REDIS_ADDR=localhost:6379 \
JANUS_AUTH_ENABLED=false ./janus-api

4. Configure Janus

Janus reads configuration from environment variables (highest priority) and/or a YAML config file. The Compose file already wires up sensible defaults for local development.

Environment Variables

The most commonly tuned variables (all prefixed JANUS_):

VariableDefaultDescription
JANUS_CONFIG_FILEPath to a YAML config file (overrides all of the below)
JANUS_HTTP_PORT8080HTTP REST API port
JANUS_GRPC_PORT9090gRPC API port
JANUS_PG_HOSTlocalhostPostgreSQL host
JANUS_PG_PORT5432PostgreSQL port
JANUS_PG_USER / PASSWORD / DATABASEjanusPostgreSQL credentials
JANUS_NATS_URLnats://localhost:4222NATS connection URL
JANUS_REDIS_ADDRlocalhost:6379Redis address (rate-limit & heartbeat)
JANUS_AUTH_ENABLEDtrueEnable API key authentication
JANUS_MIGRATION_AUTOfalseAuto-run DB migrations on boot
JANUS_TLS_ENABLEDfalseEnable TLS (set with CERT_FILE / KEY_FILE / CLIENT_CA_FILE)

YAML Config File

For complex deployments, point Janus at a YAML file via JANUS_CONFIG_FILE. Start from the reference file:

cp configs/janus.example.yaml configs/janus.local.yaml

# Then run with:
JANUS_CONFIG_FILE=configs/janus.local.yaml ./janus-api

The YAML mirrors the environment variables and adds structured sections for the outbox, heartbeat, and observability:

server:
  http_port: 8080
  grpc_port: 9090

postgres:
  host: localhost
  port: 5432
  user: janus
  password: ${JANUS_PG_PASSWORD}   # env interpolation supported
  database: janus
  max_conns: 20

nats:
  url: "nats://localhost:4222"

cache:
  driver: redis
  addr: "localhost:6379"

log:
  level: info        # debug | info | warn | error
  format: json       # json | text

migration:
  auto: true
  path: migrations/

outbox:
  enable: true
  batch_size: 100
  max_retries: 5     # retry exhausted → dead letter queue

observability:
  metrics:
    enabled: true
    path: /metrics
  tracing:
    enabled: false
    endpoint: ""     # OTLP collector endpoint

Project Config (Declarative)

A second YAML file — the project config — declares your agents, budgets, and governance policies as code. It complements the server config above (connections & runtime): the project file defines your tenant topology. Apply it with the Janus CLI:

# 1. Generate a starter janus.project.yaml in the current directory
janus project init

# 2. Edit it to declare agents, budgets, and policies
#    (full schema: configs/janus.project.example.yaml)

# 3. Preview the changes without applying them
janus project diff

# 4. Apply (idempotent — safe to re-run in CI/CD)
janus project apply

How the CLI finds the project file — resolved in this order (first hit wins):

PrioritySourceExample
1--file flagjanus project apply --file configs/janus.project.yaml
2JANUS_PROJECT_FILE env varexport JANUS_PROJECT_FILE=configs/janus.project.yaml
3janus.project.yaml in the current directory (default)run janus project init to create it

JANUS_PROJECT_FILE tells the CLI where to look — the API server does not read it directly. End-to-end example:

# Generate the starter file
janus project init

# Declare agents, budgets, and policies in the YAML (see below)

# Apply the file explicitly by path
export JANUS_PROJECT_FILE=configs/janus.project.yaml
janus project diff      # review
janus project apply     # apply (idempotent)

A project file declares the full tenant topology — agents and their capabilities, per-tenant and per-team budgets, and allow/deny/approve policies:

version: v1
default_tenant: acme

defaults:
  protocol: custom-sdk
  mailbox:
    ack_wait_seconds: 300
    max_deliver: 5
  policy:
    priority: 100

tenants:
  acme:
    name: Acme Engineering

    agents:
      code-review:
        team: engineering
        capabilities:
          - id: code_review
            data_classifications: [public, internal, confidential]
        concurrency: 4

    budgets:
      tenant:
        tpm: 2000000# tokens per minute
      teams:
        engineering:
          concurrency: 20
          tpm: 600000

    policies:
      approve:
        capabilities: [prod_deploy]# require human approval
      allow:
        - agent: code-review
          capability: code_review
      deny:
        - agent: intern-agent
          tool: deploy.prod
      data_classification:
        deny:
          - team: interns
            classifications: [confidential, restricted]

Full schema: configs/janus.project.example.yaml in the repo.

Field Reference

Every field maps to a Janus core resource. The tables below explain each level of the file.

Top level

FieldTypeDescription
versionstringSchema version. Currently v1. Required.
default_tenantstringTenant targeted when --tenant isn't passed. Must exist under tenants.
defaultsobjectValues inherited by every tenant and agent unless overridden locally.
tenantsmapTenant declarations, keyed by tenant ID. Required.

defaults — inherited by all tenants:

FieldDefaultDescription
protocolcustom-sdkAgent communication protocol. Applied when an agent doesn't set its own.
classificationDefault data classification. One of: public, internal, confidential, restricted.
mailbox.ack_wait_secondsSeconds before an unacked task is redelivered to the mailbox.
mailbox.max_deliverMax delivery attempts before the task moves to the dead letter queue.
mailbox.retention_secondsHow long completed mailbox state is retained.
capacity.max_concurrency1Default max concurrent in-flight tasks per agent.
policy.priorityPriority assigned to auto-generated policy rules (higher = evaluated first).

Tenant (tenants.<id>):

FieldTypeDescription
namestringHuman-readable display name.
agentsmapAgent declarations keyed by agent ID.
budgetsobjectCost and rate limits at tenant, team, agent, model, and task scope.
policiesobjectAllow, deny, approval, and data-classification rules compiled into policy templates.

Agent (tenants.<id>.agents.<id>):

FieldTypeDescription
namestringDisplay name. Defaults to the agent ID.
teamstringTeam ID. Enables team-level budgets and policy targeting.
protocolstringOverrides defaults.protocol for this agent.
endpointstringCallback URL where the agent receives task notifications.
descriptionstringFree-text description of what the agent does.
capabilitieslistCapabilities this agent can handle. Each entry is a string or an object with id, description, and data_classifications. Required — at least one.
concurrencyintMax concurrent in-flight tasks. Overrides capacity.max_concurrency.
rpm / tpmintRequests-per-minute and tokens-per-minute metadata for budget enforcement.
mailboxobjectPer-agent mailbox override. If omitted, a default mailbox <agent-id>.default is auto-created.

Budget limits — each budget scope accepts the same fields:

FieldDescription
rpmMax requests per minute.
tpmMax tokens per minute.
concurrencyMax concurrent operations.
daily_usdPlanned. Not yet enforced — awaiting trusted token metering.
monthly_usdPlanned. Not yet enforced — awaiting trusted token metering.

Budget scopes: tenant (whole tenant), teams.<id>, agents.<id>, models.<id>, model_providers.<id>, tasks.<id>.

Policies — compiled into policy rules via templates:

FieldDescription
approve.capabilitiesCapabilities that require human approval before execution.
approve.toolsTools that require human approval before invocation.
allow / denyBindings of agent or team to a capability or tool. A binding must set exactly one of agent/team and one of capability/tool.
data_classification.allow/denyControls which data classifications an agent or team may receive.

Multi-Agent Example

A realistic project file with three agents across two teams, per-team budgets, and governance policies:

version: v1
default_tenant: acme

defaults:
  protocol: custom-sdk
  mailbox:
    ack_wait_seconds: 300
    max_deliver: 5
  policy:
    priority: 100

tenants:
  acme:
    name: Acme Engineering
    agents:
      code-review:          # agent 1: reviews pull requests
        team: engineering
        capabilities:
          - id: code_review
            data_classifications: [public, internal, confidential]
        concurrency: 4
      test-runner:           # agent 2: runs test suites
        team: engineering
        capabilities: [test_run]
        concurrency: 8
      deploy-bot:            # agent 3: production deployments
        team: sre
        endpoint: https://deploy.internal.acme.com
        capabilities:
          - id: prod_deploy
            description: Deploy to production
        concurrency: 1
        mailbox:
          ack_wait_seconds: 600    # deploys take longer
    budgets:
      tenant:
        tpm: 2000000
      teams:
        engineering:
          concurrency: 20
          tpm: 600000
        sre:
          concurrency: 4
    policies:
      approve:
        capabilities: [prod_deploy]   # require human approval
        tools: [deploy.prod]
      allow:
        - agent: code-review
          capability: code_review
        - agent: test-runner
          capability: test_run
      deny:
        - agent: deploy-bot
          tool: deploy.prod          # blocked unless approved
      data_classification:
        deny:
          - team: interns
            classifications: [confidential, restricted]
The project file is declarative and idempotent — janus project apply creates missing resources and updates existing ones without deleting anything. Run janus project diff first to preview changes.

Adding Agents Dynamically

The project file is not the only entry point — you can add an agent to an existing tenant at any time with janus agent add. It registers the agent, creates its default mailbox, and persists the change back to janus.project.yaml:

janus agent add summarizer --tenant acme --team engineering --capability summarize
# → Agent summarizer added to tenant acme and saved to janus.project.yaml

Repeat --capability to grant multiple capabilities, use --classification to restrict data classifications per capability, and omit --mailbox to auto-create the <agent-id>.default mailbox:

janus agent add indexer --tenant acme --endpoint https://idx.internal.acme.com --capability index --capability search --concurrency 4
janus agent status indexer --tenant acme
janus agent heartbeat indexer --tenant acme

For a one-off runtime registration that is not written to the project file, use janus agent register. --protocol accepts a2a, acp, or custom-sdk (default a2a):

janus agent register --id adhoc-worker --name "Adhoc Worker" --protocol custom-sdk

Authentication

Every API call to Janus is authenticated with a tenant-scoped API key (enabled by default). Optionally, mTLS can be layered on for stricter machine-to-machine deployments.

API Keys

With auth.enabled on (the default), every request must present a valid key via the X-API-Key or Authorization: Bearer <key> header. Keys are bound to one tenant — a key issued for tenant A cannot reach tenant B (the server rejects the call with 403).

Create a key with the CLI. The raw key — janus_<64 hex chars> — is returned only once, so store it immediately:

janus --tenant acme api-key create --name ci-bot
# → {"tenant_id":"acme","name":"ci-bot","prefix":"janus_a1","key":"janus_a1b2c3...","created_at":"..."}  (store the key — shown only once)

The server keeps only a SHA-256 hash of the key plus its 8-character prefix for lookup, so the raw key can never be recovered. List and revoke keys the same way:

janus --tenant acme api-key list
janus --tenant acme api-key revoke <key-id>

Point the CLI or SDK at a key with --api-key or the JANUS_API_KEY environment variable, and pass it on plain HTTP calls as a header:

export JANUS_API_KEY=janus_a1b2c3d4e5…
curl -H "Authorization: Bearer $JANUS_API_KEY" http://localhost:8080/v1/tenants/acme/tasks

mTLS (optional)

For stricter machine-to-machine security, enable TLS and require client certificates. When client_ca_file is set, Janus verifies client certificates (mutual TLS, minimum TLS 1.2):

# configs/janus.local.yaml
auth:
  enabled: true

tls:
  enabled: true
  cert_file: /etc/janus/server.crt
  key_file: /etc/janus/server.key
  client_ca_file: /etc/janus/ca.crt   # set to require client certificates (mTLS)

Or with environment variables (all JANUS_-prefixed):

export JANUS_AUTH_ENABLED=true
export JANUS_TLS_ENABLED=true
export JANUS_TLS_CERT_FILE=/etc/janus/server.crt
export JANUS_TLS_KEY_FILE=/etc/janus/server.key
export JANUS_TLS_CLIENT_CA_FILE=/etc/janus/ca.crt

Keep auth.enabled on in production — when disabled, every API endpoint is unauthenticated.

LLM for Intent Routing (optional)

To enable target_type: "intent" (natural language → capability routing), configure an OpenAI-compatible LLM:

# Environment variables
export JANUS_LLM_ENABLED=true
export JANUS_LLM_API_KEY=sk-xxx
export JANUS_LLM_MODEL=gpt-4o-mini
export JANUS_LLM_BASE_URL=https://api.openai.com/v1  # or Ollama/vLLM/Azure

When LLM is not configured, target_type: "intent" falls back to keyword matching (no external dependency). When configured, Janus maps natural language to the best matching capability via the LLM, validates the result against the online agent catalog, then routes deterministically.

5. Install the Python SDK

pip install janus-broker

6. Publish a Task

Create a file publish.py:

from janus_broker import JanusClient

client = JanusClient("http://localhost:8080", tenant_id="acme")

# Create a mailbox for the reviewer agent
client.create_mailbox("review-mb", agent_id="reviewer")

# Publish a task to the mailbox
resp = client.publish_task({
    "id": "task-001",
    "source_agent": "product",
    "target_type": "mailbox",
    "target_value": "review-mb",
    "envelope": {
        "type": "code_review",
        "payload": {"pr_url": "https://github.com/org/repo/pull/42"},
        "priority": "high"
    }
})
print("Published:", resp.task_id)
python publish.py

Target Types

Janus supports five ways to route a task to its destination:

  • mailbox — deliver directly to a specific mailbox. Best for point-to-point workflows.
  • agent — deliver to a specific agent. Janus resolves agent-2 to its active mailbox automatically.
  • capability — describe what you need, Janus finds an online agent with that capability.
  • group — deliver to a team. Set team: "sre" in your agent config, then publish with target_type: "group", target_value: "sre".
  • intent — describe what you need in natural language. Requires LLM configuration (JANUS_LLM_ENABLED=true). Janus maps your request to the best matching capability.
# Example: to a specific agent
client.publish_task({
    "id": "task-002",
    "source_agent": "product",
    "target_type": "agent",
    "target_value": "reviewer",
    "envelope": {"type": "code_review", "payload": {"pr_url": "https://github.com/org/repo/pull/42"}, "priority": "high"}
})

# Example: by capability (intent-driven)
client.publish_task({
    "id": "task-003",
    "source_agent": "product",
    "target_type": "capability",
    "target_value": "go-code-review",
    "envelope": {"type": "go_pr_review", "payload": {"repo": "myapp"}, "priority": "normal"}
})

# Example: to a team (group routing)
client.publish_task({
    "id": "task-004",
    "source_agent": "product",
    "target_type": "group",
    "target_value": "sre",
    "envelope": {"type": "deploy", "payload": {"version": "2.0.0"}, "priority": "high"}
})

7. Pull and Complete the Task

Create a file worker.py:

Report progress (new in v1.4)

Agents report mid-task progress like a log statement — the Worker injects the callback automatically:

def process(task, progress):
    progress("Analyzing code...", percent=20)
    issues = analyze(task.payload)
    progress("Fixing issues...", percent=60)
    return WorkerResult(output={...})

# Subscribe from the other side (real-time SSE):
for evt in client.stream_task("task-001"):
    print(f'[{evt["payload"].get("percent")}%] {evt["payload"]["message"]}')
# [20%] Analyzing code...
# [60%] Fixing issues...
from janus_broker import JanusClient

client = JanusClient("http://localhost:8080", tenant_id="acme")

# Pull task from mailbox
result = client.pull_task("review-mb", "reviewer")
print(f"Got task: {result.task.id}")

# Start processing (acquire lease)
client.start_task(result.task.id, result.lease.lease_id)

# Process... (in real code, do the work here)

# Acknowledge completion
client.ack_task(result.task.id, {
    "lease_id": result.lease.lease_id,
    "result_ref": "s3://review-result.json"
})
print("Task completed")
python worker.py

8. Verify via CLI

Use the Janus CLI to check task state:

janus task list --tenant acme
# → task-001  completed  2026-07-31T12:00:00Z

janus mailbox list --tenant acme
# → review-mb  tasks: 1  pending: 0

CLI Reference

The janus CLI is the primary tool for managing agents, tasks, mailboxes, API keys, policies, and project configuration. It talks to the Janus API server over HTTP — point it at your server with --server.

Global Flags

Every command accepts these persistent flags. Set them once via environment variables to avoid repetition:

FlagDefaultDescription
--serverhttp://localhost:8080Janus API server URL.
--tenantdefaultTenant ID to operate on.
--api-key$JANUS_API_KEYAPI key for authentication. Falls back to the environment variable.
--filePath to a project config file. Overrides $JANUS_PROJECT_FILE and the default janus.project.yaml.
# One-time setup for your shell session
export JANUS_API_KEY="jak_..."
export JANUS_PROJECT_FILE=configs/janus.project.yaml

# Now every janus command picks these up automatically
janus task list --tenant acme

Command Groups

CommandSubcommandsPurpose
projectinit, validate, diff, apply, syncDeclarative tenant topology management.
tenantaddCreate a tenant and persist it to the project file.
agentregister, add, status, heartbeatRegister agents and check liveness.
taskpublish, status, cancel, replay, eventsPublish and inspect tasks.
mailboxcreate, status, pause, resume, pull, ack, nackManage mailboxes and process tasks.
api-keycreate, list, revokeTenant-scoped API key lifecycle.
policyallow-agent, deny-agent, allow-team, deny-team, require-approval, allow-classification, deny-classification, allow-tool, deny-toolGenerate governance policy rules from templates.
dlqquery, replay, discardDead letter queue inspection and recovery.
dashboardInteractive TUI for monitoring tasks, mailboxes, and agents.

Project Workflow

The project commands manage your tenant topology as code (covered in Configure):

janus project init                    # generate janus.project.yaml
janus project validate                # check the file for errors
janus project diff                     # preview changes vs the live API
janus project apply --all-tenants      # apply every tenant
janus project sync --overwrite          # pull live resources back into the file

API Keys

API keys are tenant-scoped. Create one per environment or service and revoke them when rotated. The raw key is shown only once:

janus --tenant acme api-key create --name ci-bot
# → {"tenant_id":"acme","name":"ci-bot","prefix":"janus_a1","key":"janus_a1b2c3...","created_at":"..."}  (store the key — shown only once)

janus --tenant acme api-key list

janus --tenant acme api-key revoke <key-id>

Tasks & Mailboxes

Publish, pull, and acknowledge tasks — the same lifecycle the SDK wraps, but from the shell:

# Publish a task to a mailbox
janus task publish --tenant acme --source product --mailbox review-mb

# Check task status
janus task status task-001 --tenant acme

# Pull the next task from a mailbox
janus mailbox pull review-mb --agent reviewer

# Inspect the dead letter queue and replay
janus dlq query --tenant acme
janus dlq replay task-001 --tenant acme

Dashboard

Launch an interactive terminal dashboard to monitor tasks, mailboxes, and agent health in real time:

janus dashboard --server http://localhost:8080 --tenant acme

Production Deployment

For Kubernetes, Janus ships a Helm chart with liveness/readiness probes, a migration Job hook, HPA, PDB, and Prometheus scrape annotations:

helm install janus deployments/helm/janus-core/

See the GitHub repo for the full chart values and the ops runbook.

Distributed Deployment

Janus Core is a stateless compute node — all persistent state is externalized to PostgreSQL, NATS JetStream, and Redis. You can run multiple replicas horizontally; any replica can serve any request, a single replica crash loses no business state, and traffic is balanced behind a front load balancer.

Load Balancer / Ingress
Janus Core (stateless)
#1 #2 #N
HPA · 2–10 replicas
PostgreSQLpersistent state
NATS JetStreamevents / messaging
Redisrate limit / cache
The compute tier scales horizontally; all state is externalized to PG / NATS / Redis.

When multiple replicas consume outbox events concurrently, a database-level lease provides mutual exclusion so events are neither double-delivered nor lost. Migration 000011_outbox_worker_lease adds lease columns to outbox_events, and 000012_outbox_dedupe_key adds an idempotency key:

locked_by        text         -- worker that claimed the row
locked_at        timestamptz
lease_expires_at timestamptz  -- reclaimable by another worker after expiry
dedupe_key       text         -- unique (tenant_id, dedupe_key) prevents double delivery

If a replica crashes, its locked rows are reclaimed by a live replica once the lease expires; dedupe_key guarantees retries and concurrent schedulers never insert the same delivery twice.

Scaling the Compute Tier

The Helm chart enables the HorizontalPodAutoscaler by default, auto-scaling on CPU and memory:

autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 70
  targetMemoryUtilizationPercentage: 80

A PodDisruptionBudget (minAvailable: 1) keeps at least one replica available during rollouts and node maintenance.

Scaling the State Tier

As the compute tier scales horizontally, each state store scales its own way:

  • PostgreSQL — a single primary handles Janus's workload; add PgBouncer for higher throughput, and read replicas for read-heavy access patterns.
  • NATS JetStream — natively supports clustering with RAFT consensus; scale JetStream nodes horizontally for more messaging throughput and fault tolerance.
  • Redis — a single instance is usually enough; use Redis Cluster or Sentinel when you need higher availability.
Janus Core runs with a readOnlyRootFilesystem; the only persistent volume is for artifact storage (artifacts) — it holds no runtime state, so replicas are stateless and replaceable at any time.

Complete Example: Smart Customer Service Center

This section walks through a real-world multi-agent scenario — handling a customer complaint about a wrong item delivery — to show how Janus's routing, durability, and governance features work together in a production-like workflow.

What You'll Learn

  • Using target_type: intent with LLM-powered natural language routing
  • Capability-based routing (target_type: capability)
  • Team-based routing (target_type: group)
  • Task lease expiry and automatic recovery when an agent crashes
  • Parallel task fan-out and barrier-style synchronization
  • Data passing between agents (Agent A's output becomes Agent B's input)

The Scenario

A customer reports: "I ordered a red phone but received a blue one. I need an exchange."

Janus coordinates four agents to resolve this:

  1. Logistics Bot — investigates the original order (capability: wrong_item_investigate, team: logistics)
  2. Shipping Bot — ships the correct red phone (capability: reship, team: warehouse)
  3. Return Bot — arranges pickup of the wrong blue phone (capability: retrieve_wrong, team: warehouse)
  4. Notify Bot — sends status updates to the customer (capability: notify, team: support)

The Flow (With Failure Injection)

Customer: "I ordered red, got blue"
    │
    ▼  intent LLM → "wrong_item_investigate"
    │
Logistics-bot ─── pulls order ─── 💀 crashes (no ACK) ─── lease expires ─── recovers ─── ACKs
    │
    │  data: {order: "ORD-789", ordered: "red", shipped: "blue"}
    ▼
    ├── ► Shipping-bot ─── ships correct red ─── ACK {tracking: "SF789"}
    ├── ► Return-bot   ─── picks up wrong blue ─── ACK {pickup: "PU123"}
    └── ► Notify-bot   ─── "exchange started" ─── ACK
            │
            ▼
         Notify-bot ─── "red shipped SF789, blue pickup PU123" ─── ACK

Key design: The logistics-bot's investigation result (which item is correct, which is wrong) drives the downstream agents — the shipping-bot reads the investigation data to know what to ship; the return-bot reads it to know what to retrieve. Agents don't just acknowledge tasks — they produce data that other agents consume.

Step 1: Register Agents and Their Capabilities

Each agent declares what it can do (capability) and which team it belongs to (team). Janus uses capabilities for matching and teams for group routing.

# Register the four agents
for agent in [
  {"id": "logistics-bot", "display_name": "Logistics Bot", "team": "logistics"},
  {"id": "shipping-bot", "display_name": "Shipping Bot", "team": "warehouse"},
  {"id": "return-bot", "display_name": "Return Bot", "team": "warehouse"},
  {"id": "notify-bot", "display_name": "Notify Bot", "team": "support"}
]:
    client.create_agent(agent["id"], agent["display_name"], agent["team"])

Then assign capabilities and create mailboxes (task queues) for each agent:

# Capabilities: what each agent can do
caps = {
  "logistics-bot": "wrong_item_investigate",
  "shipping-bot":  "reship",
  "return-bot":    "retrieve_wrong",
  "notify-bot":    "notify",
}
# Create mailboxes (each with a 5-second ACK timeout for logistics)
client.create_mailbox("logistics-mb", "logistics-bot", ack_wait=5)
client.create_mailbox("shipping-mb", "shipping-bot")
client.create_mailbox("return-mb", "return-bot")
client.create_mailbox("notify-mb", "notify-bot")

Step 2: Publish with Intent Routing (LLM-Powered)

Instead of specifying a concrete agent or capability, use target_type: intent with natural language. Janus's LLM-powered resolver maps the request to the best matching capability:

# Customer complaint → LLM resolves to "wrong_item_investigate"
task = client.publish_task({
  "id":           "task-001",
  "source_agent": "customer",
  "target_type": "intent",
  "target_value": "received wrong item, ordered red but got blue, need exchange",
  "envelope":     {"type": "complaint", "payload": {"order": "ORD-789"}}
})
# Janus automatically resolves:
#   target_type: "capability"
#   target_value: "wrong_item_investigate"
#   → routes to logistics-mb (logistics-bot's mailbox)

Step 3: Lease Expiry — Testing Durability

Janus guarantees at-least-once delivery. When an agent pulls a task and crashes without ACKing, the lease expires and the task automatically returns to the queue:

# logistics-bot pulls the task...
delivery = client.pull_task("logistics-mb", "logistics-bot")

# 💀 Simulate a crash — do NOT ACK. Just stop.
# After the lease expires (5 seconds with our config),
# Janus automatically re-delivers the task to the mailbox.

# When the agent recovers and pulls again:
delivery = client.pull_task("logistics-mb", "logistics-bot")
# ← Same task! Janus redelivered it after lease expiry.

# Now ACK with investigation results (data for downstream agents):
client.ack_task(delivery.task_id, delivery.lease_id, 
  result_ref='{ "confirmed": true, "order": "ORD-789", "ordered": "red", "shipped": "blue" }')

Step 4: Fan-Out — Three Parallel Branches

After confirming the wrong item, Janus orchestrates three parallel workflows. Each branch routes differently — capability routing for shipping/return, group routing for notifications:

# Branch A: Ship the correct item (capability routing)
client.publish_task({
  "id": "ship-001", "source_agent": "logistics-bot",
  "target_type": "capability", "target_value": "reship",
  "envelope": {"payload": {"order": "ORD-789", "correct": "red"}}
})
# → Janus finds shipping-bot (capability: reship) and routes to shipping-mb

# Branch B: Retrieve the wrong item (capability routing)
client.publish_task({
  "id": "ret-001", "source_agent": "logistics-bot",
  "target_type": "capability", "target_value": "retrieve_wrong",
  "envelope": {"payload": {"order": "ORD-789", "wrong": "blue"}}
})

# Branch C: Notify customer (capability routing to support team)
client.publish_task({
  "id": "notify-001", "source_agent": "logistics-bot",
  "target_type": "capability", "target_value": "notify",
  "envelope": {"payload": {"message": "exchange started"}}
})

Step 5: Each Agent Processes Its Branch

Each agent independently pulls, processes, and ACKs its task, producing result data that flows to downstream agents:

# Shipping-bot: pull → process → ACK with tracking number
delivery = client.pull_task("shipping-mb", "shipping-bot")
client.ack_task(delivery.task_id, delivery.lease_id,
  result_ref='{ "tracking": "SF789", "reshipped": true }')

# Return-bot: pull → process → ACK with pickup info
delivery = client.pull_task("return-mb", "return-bot")
client.ack_task(delivery.task_id, delivery.lease_id,
  result_ref='{ "pickup": "PU123", "scheduled": true }')

# Notify-bot: pull → ACK (initial notification)
delivery = client.pull_task("notify-mb", "notify-bot")
client.ack_task(delivery.task_id, delivery.lease_id)

Step 6: Data Fan-In — Combine Results

After both shipping and return complete, publish a final notification with both results combined — demonstrating how agents pass data through the system:

# Final notification combines shipping + return results
client.publish_task({
  "id": "notify-final", "source_agent": "shipping-bot",
  "target_type": "capability", "target_value": "notify",
  "envelope": {"payload": {
    "shipping": {"tracking": "SF789"},
    "return":   {"pickup": "PU123"},
    "message": "Red phone shipped (SF789), blue phone pickup (PU123)"
}})
What this example demonstrates: This is a complete production-like workflow covering intent routing (LLM-powered NL → capability), capability routing (find the right agent by what it can do), multiple routing types in one pipeline, lease expiry recovery (crash simulation), parallel branches, and data passing between agents. The same patterns apply to customer support, order fulfillment, claims processing, and any multi-agent workflow where agents need to collaborate and pass data.

What's Next

Janus runs with auth enabled by default. The Compose file sets JANUS_AUTH_ENABLED=false for local convenience — re-enable it (and create an API key via janus api-key create) before exposing Janus to a network.