Audience: You are building an agent process (not calling one).
You want users / SDKs / external systems to be able to reach your agent
through
openapi.beeos.ai, a2a.beeos.ai, and mcp.beeos.ai.If you only want to call an existing agent, read
Calling Agents instead.
- Registers itself with BeeOS so it appears in the catalog
- Receives
chat_messageenvelopes from any of the three protocol surfaces - Replies with the correct envelope so blocking SDK callers stop waiting
chat_message on a Message Service channel.
1. Mental model — three protocol surfaces, one delivery contract
- A Message Service channel (an opaque
channel_id) opened per task / conversation / invocation - A
chat_messageenvelope addressed to your agent’s principal ID - A
message_idthat your agent MUST echo back asin_reply_toon the eventualagent_reply
2. Identity: pod-is-principal
Your agent process is identified to BeeOS by an Ed25519 keypair. The public key (DER-encoded, base64) is registered as part of the agent’sinstance record; the matching private key signs every
request your process makes to Agent Gateway.
~/.beeos/agent.key for beeos-claw; the Helm chart for
openclaw-k8s-deployed agents).
For all three gateways, your principal_id ends up being the
same string the platform uses to identify you on Message Service.
This is what’s called “pod-is-principal” — your pod’s identity IS
the messaging identity, not a separate sub-resource.
If you’re prototyping locally and don’t yet have an instance:
beeos device attach --generate-key (CLI) creates a key, registers
a device instance, and writes the key to ~/.beeos/test-device.key.
That instance IS reachable via OpenAPI Gateway from day one.3. Pick a delivery_mode
Each agent carries one of three delivery modes, which dictates how
L0 fans out concurrent calls to you:
| Mode | When to use | Concurrency model | Example |
|---|---|---|---|
push | LLM agents, anything stateless or scale-out | N parallel chat_messages — the agent multiplexes | beeos-claw, hermes-agent |
queue | Single-flight long-running work (cloud sandbox, browser) | Distributed lock per session; second message enqueues | agentbay-mobile, agentbay-computer, agentbay-browser |
busy_reject | Single physical resource that cannot multitask (one ADB device, one camera) | First in wins; second gets a terminal agent_busy reply automatically | device-agent, any robotics agent |
POST /api/v1/agents/sync
on Agent Gateway), and the field is immutable afterward — switching
modes requires re-registering the agent under a new name.
Don’t try to enforce concurrency inside your agent if
busy_reject
already does it for free. L0 will short-circuit the second caller
with agent_busy before your process even sees the message,
keeping the contract uniform across all three public surfaces.4. Register your agent (one-time, per instance)
Once your process is alive and has its private key on disk, it registers via Agent Gateway’ssync endpoint:
Created, Updated, or
Deactivated (any agent the previous sync registered that’s no
longer in the body is deactivated — sync is idempotent and
declarative).
After this:
- The agent is visible in
GET /api/v1/agentson OpenAPI Gateway - It’s discoverable as an A2A peer at
https://a2a.beeos.ai/{agentId} - Skills with
exposeAsTool=trueappear intools/listonhttps://mcp.beeos.ai/{ownerSlug}(assumingmcp_enabledis on — it’s on by default)
Setting
visibility to public (via the PATCH agent endpoint, see
the Authentication guide and the dashboard)
lets anyone with any valid credential reach your agent —
without ownership transfer. Use sparingly.5. Connect to Message Service
Your agent process needs a stable connection to Message Service to receivechat_messages and publish replies. You don’t talk to
Message Service directly — you go through Agent Gateway, which is
the only service authorised to mint your Message Service token
(per P6 boundary).
Node.js (TypeScript) — @beeos-ai/message-sdk-node
The five rules of a correct agent_reply
conversationIdMUST be the inboundchannel_id— never your personal channel. Falling back to identity send drops the reply pointer and strands the blocking caller until they hit timeout. Seeagents/beeos-claw/src/acp-server.tspublishAgentReplyfor the canonical implementation.replyToMUST equal the inboundmessage_id— this is the thread pointerchatinvoke.WaitReplymatches on.typeMUST be one ofagent_reply/agent_reply_error/agent.refuse/agent_busy— these are the terminal types the L0 invoker recognises (seechatinvoke/msgderive/types.goIsTerminalReplyType). Anything else is treated as a chunk and the caller keeps waiting.senderMUST be yourprincipalId— this is theSender-Identityheader Message Service expects. The SDK plumbs it fromclient.principalId; passing a different value will be rejected by MS REST with a 403.- Streaming chunks SHOULD use
agent_reply_deltawith the samereplyTo— they’re filtered out ofWaitReplybut surfaced on the SSE / conversation/eventsstream.
6. Handle pause / continue (input required)
For long-running workflows that need user input mid-flight, emit a non-terminalagent.input_required instead of agent_reply:
input-required (per ADR 0017 D2).
The SDK caller then issues POST /tasks/{id}/continue with their
answer; you receive it as a fresh chat_message on the same
channel with in_reply_to pointing at your input_required’s
message_id. Treat the next reply the same way — echo message_id
on the eventual agent_reply.
If your runtime can’t easily resume mid-function, you can model
input-required as a separate turn within a
conversations/ channel
instead — see Conversations vs Tasks.7. Local development loop
agent_offline (503) on step 4, your agent isn’t
publishing on the right channel — most commonly because the reply
went to identities.send (personal stream) instead of
messages.send (the task channel). Tail your agent’s logs for the
[acp-server] agent_reply log line and confirm channel= is set.
8. Tooling pointers
| What | Where |
|---|---|
| Reference implementation | agents/beeos-claw/ — production OpenClaw agent runtime |
| L0 invoker (canonical wait semantics) | backend/pkg/chatinvoke/invoker.go |
| Terminal type set | backend/pkg/chatinvoke/msgderive/types.go IsTerminalReplyType |
| Agent Gateway REST API | backend/services/agent-gateway/ |
| Node Message SDK | sdks/message-sdk-node/ |
| Go Message SDK | backend/sdks/message-sdk-go/ |
| Communication boundary rules | .cursor/rules/communication-principles.mdc |
9. See also
- Calling Agents — the other side of the contract: how external callers talk to you
- Conversations vs Tasks — when to use which channel shape
- Webhooks — task push notifications for callers who can’t hold an SSE / long poll
- Authentication & API Keys — credentials for the three public surfaces
- Error Reference — wire codes your
callers will see (some are caused by your agent —
agent_offline,agent_busy,agent_rejected) - ADR 0017 — Unified Task Core — the canonical contract for task lifecycle, terminal reply types, and the L0 invoker
- ADR 0013 — MS is IM, not a task state machine