Install, configure, and run Janus, then complete your first agent-to-agent task handoff.
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 binarydocker-compose.yml — one command brings up Janus + PostgreSQL + NATS + Redisconfigs/janus.example.yaml — reference configuration filemigrations/ — PostgreSQL schema migrations (auto-applied on boot)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 gateways9090 — gRPC APITo 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
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.
The most commonly tuned variables (all prefixed JANUS_):
| Variable | Default | Description |
|---|---|---|
JANUS_CONFIG_FILE | — | Path to a YAML config file (overrides all of the below) |
JANUS_HTTP_PORT | 8080 | HTTP REST API port |
JANUS_GRPC_PORT | 9090 | gRPC API port |
JANUS_PG_HOST | localhost | PostgreSQL host |
JANUS_PG_PORT | 5432 | PostgreSQL port |
JANUS_PG_USER / PASSWORD / DATABASE | janus | PostgreSQL credentials |
JANUS_NATS_URL | nats://localhost:4222 | NATS connection URL |
JANUS_REDIS_ADDR | localhost:6379 | Redis address (rate-limit & heartbeat) |
JANUS_AUTH_ENABLED | true | Enable API key authentication |
JANUS_MIGRATION_AUTO | false | Auto-run DB migrations on boot |
JANUS_TLS_ENABLED | false | Enable TLS (set with CERT_FILE / KEY_FILE / CLIENT_CA_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
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):
| Priority | Source | Example |
|---|---|---|
| 1 | --file flag | janus project apply --file configs/janus.project.yaml |
| 2 | JANUS_PROJECT_FILE env var | export JANUS_PROJECT_FILE=configs/janus.project.yaml |
| 3 | janus.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.
Every field maps to a Janus core resource. The tables below explain each level of the file.
Top level
| Field | Type | Description |
|---|---|---|
version | string | Schema version. Currently v1. Required. |
default_tenant | string | Tenant targeted when --tenant isn't passed. Must exist under tenants. |
defaults | object | Values inherited by every tenant and agent unless overridden locally. |
tenants | map | Tenant declarations, keyed by tenant ID. Required. |
defaults — inherited by all tenants:
| Field | Default | Description |
|---|---|---|
protocol | custom-sdk | Agent communication protocol. Applied when an agent doesn't set its own. |
classification | — | Default data classification. One of: public, internal, confidential, restricted. |
mailbox.ack_wait_seconds | — | Seconds before an unacked task is redelivered to the mailbox. |
mailbox.max_deliver | — | Max delivery attempts before the task moves to the dead letter queue. |
mailbox.retention_seconds | — | How long completed mailbox state is retained. |
capacity.max_concurrency | 1 | Default max concurrent in-flight tasks per agent. |
policy.priority | — | Priority assigned to auto-generated policy rules (higher = evaluated first). |
Tenant (tenants.<id>):
| Field | Type | Description |
|---|---|---|
name | string | Human-readable display name. |
agents | map | Agent declarations keyed by agent ID. |
budgets | object | Cost and rate limits at tenant, team, agent, model, and task scope. |
policies | object | Allow, deny, approval, and data-classification rules compiled into policy templates. |
Agent (tenants.<id>.agents.<id>):
| Field | Type | Description |
|---|---|---|
name | string | Display name. Defaults to the agent ID. |
team | string | Team ID. Enables team-level budgets and policy targeting. |
protocol | string | Overrides defaults.protocol for this agent. |
endpoint | string | Callback URL where the agent receives task notifications. |
description | string | Free-text description of what the agent does. |
capabilities | list | Capabilities this agent can handle. Each entry is a string or an object with id, description, and data_classifications. Required — at least one. |
concurrency | int | Max concurrent in-flight tasks. Overrides capacity.max_concurrency. |
rpm / tpm | int | Requests-per-minute and tokens-per-minute metadata for budget enforcement. |
mailbox | object | Per-agent mailbox override. If omitted, a default mailbox <agent-id>.default is auto-created. |
Budget limits — each budget scope accepts the same fields:
| Field | Description |
|---|---|
rpm | Max requests per minute. |
tpm | Max tokens per minute. |
concurrency | Max concurrent operations. |
daily_usd | Planned. Not yet enforced — awaiting trusted token metering. |
monthly_usd | Planned. 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:
| Field | Description |
|---|---|
approve.capabilities | Capabilities that require human approval before execution. |
approve.tools | Tools that require human approval before invocation. |
allow / deny | Bindings 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/deny | Controls which data classifications an agent or team may receive. |
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]
janus project apply creates missing resources and updates existing ones without deleting anything. Run janus project diff first to preview changes.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
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.
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
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.
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.
pip install janus-broker
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
Janus supports five ways to route a task to its destination:
agent-2 to its active mailbox automatically.team: "sre" in your agent config, then publish with target_type: "group", target_value: "sre".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"}
})
Create a file worker.py:
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
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
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.
Every command accepts these persistent flags. Set them once via environment variables to avoid repetition:
| Flag | Default | Description |
|---|---|---|
--server | http://localhost:8080 | Janus API server URL. |
--tenant | default | Tenant ID to operate on. |
--api-key | $JANUS_API_KEY | API key for authentication. Falls back to the environment variable. |
--file | — | Path 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 | Subcommands | Purpose |
|---|---|---|
project | init, validate, diff, apply, sync | Declarative tenant topology management. |
tenant | add | Create a tenant and persist it to the project file. |
agent | register, add, status, heartbeat | Register agents and check liveness. |
task | publish, status, cancel, replay, events | Publish and inspect tasks. |
mailbox | create, status, pause, resume, pull, ack, nack | Manage mailboxes and process tasks. |
api-key | create, list, revoke | Tenant-scoped API key lifecycle. |
policy | allow-agent, deny-agent, allow-team, deny-team, require-approval, allow-classification, deny-classification, allow-tool, deny-tool | Generate governance policy rules from templates. |
dlq | query, replay, discard | Dead letter queue inspection and recovery. |
dashboard | — | Interactive TUI for monitoring tasks, mailboxes, and agents. |
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 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>
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
Launch an interactive terminal dashboard to monitor tasks, mailboxes, and agent health in real time:
janus dashboard --server http://localhost:8080 --tenant acme
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.
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.
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.
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.
As the compute tier scales horizontally, each state store scales its own way:
readOnlyRootFilesystem; the only persistent volume is for artifact storage (artifacts) — it holds no runtime state, so replicas are stateless and replaceable at any time.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.
target_type: intent with LLM-powered natural language routingtarget_type: capability)target_type: group)A customer reports: "I ordered a red phone but received a blue one. I need an exchange."
Janus coordinates four agents to resolve this:
wrong_item_investigate, team: logistics)reship, team: warehouse)retrieve_wrong, team: warehouse)notify, team: support)
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.
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")
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)
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" }')
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"}}
})
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)
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)"
}})
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.