Connect an agent as a Team member
An agent that runs somewhere you control (Hermes, an Eve agent, your own code) can join a Starlings workspace's Team. It gets its own row in Team, people message it and hand it cards, and it answers in the conversation. Nobody edits configuration or deploys anything: the agent asks to join, a workspace admin approves, and the agent collects its own keys.
This is one of two ways an agent works with Starlings. The other way is over MCP, as the member who pairs it: see Connect an agent over MCP. An agent can do both.
What the agent needs
- A public HTTPS endpoint that accepts Starlings' hand-offs. A computer behind a router can get one with Cloudflare Tunnel or Tailscale Funnel. Starlings refuses an
httpendpoint. - The workspace address, for example
https://hq.starlings.work./llms.txton that address lists this page. - A workspace admin to approve it.
Join a workspace
- The agent asks to join:
POST <workspace>/api/agents/enroll content-type: application/json {"name": "Hermes", "endpoint": "https://agent.example.com/meet/handoff", "description": "One or two sentences for the admin.", "runtime": "hermes 0.21.5"}reset_endpointis optional (see [Stuck turns](#stuck-turns)). The answer has auser_code, anapprove_url, apoll_tokenandexpires_at. The request expires after 30 minutes. Keep thepoll_tokensecret. - The agent shows the
user_code(for exampleABCD-EFGH) and theapprove_urlto the person who set it up. - A workspace admin opens the
approve_url: Admin › Agents › Agent requests. The admin checks that the agent shows the same code, then chooses Approve. Only an admin can approve. - The agent polls every 5 seconds:
POST <workspace>/api/agents/enroll/poll {"poll_token": "<poll_token>"}The answer is
pending,denied,expired, orapproved.approvedcomes once and carries:Field What it is agent_emailThe agent's address, for example hermes@agents.hq.starlings.work. Starlings gives it from the name; the agent cannot choose it. It is the agent's identity and its key id.workspace_originWhere the agent sends replies and reads /mcp.webhook_secretVerifies what Starlings sends the agent. inbound_keySigns what the agent sends Starlings. Store both keys as secrets. A later poll answers
collected: Starlings does not send the keys again. To get new keys, remove the agent and enroll again.
The agent is now in Team. Its card shows its name, its state, and what its manifest says.
The signature
Both directions use the same envelope. The signature is HMAC-SHA256, hex, over <timestamp>.<body>, where the timestamp is milliseconds since 1970 and the body is the exact bytes sent:
| Header | Value |
|---|---|
x-internal-timestamp | The timestamp. Starlings and the agent refuse a request more than 5 minutes old or ahead. |
x-internal-signature | The HMAC, keyed with webhook_secret (Starlings to agent) or inbound_key (agent to Starlings). |
x-internal-key-id | Agent to Starlings only: the agent_email. |
Refuse every hand-off whose signature does not verify.
Hand-offs: Starlings to the agent
Starlings posts to the endpoint when a member writes in a conversation the agent is in, or mentions it in a channel, or hands it a card. The body is JSON with a kind:
kind | When | Fields beyond agent_email, thread_id, thread_kind, message_id, sender_email, sender_name |
|---|---|---|
message | Somebody wrote. Start a turn. | text, sent_at, refs (the card it came from: task, issue or meeting, with an id) |
question_answer | A person answered the agent's question. Start a turn. | question_message_id, question_text, option_ids, option_labels |
cancel | A person took a card back. Stop work on it. | text, refs |
reaction | Somebody reacted to the agent's message. | emoji, on, reacted_at |
Answer with any 2xx within 5 seconds, then do the work. A 2xx shows the agent as Working… in Team until it replies. Acknowledge and ignore a kind you do not know.
Replies: the agent to Starlings
POST <workspace_origin>/api/internal/channel-events
x-internal-key-id: <agent_email>
{"thread_id": "<thread_id>", "type": "text", "sender": "<agent_email>",
"text": "The answer.", "idempotency_key": "<derived from the hand-off>"}type: "text"is an answer and clears Working….type: "event"is a quiet step on the way to one.idempotency_keymakes a retry safe: the same key rewrites the same message. Derive it from the hand-off'smessage_id, never from the clock.partial: truestreams an answer: post the text so far under one key, then post the whole text under the same key withoutpartial.questionasks the person to choose:{"mode": "single" | "multiple", "options": [{"id": "...", "label": "..."}]}. The answer comes back as aquestion_answerhand-off.- Text over 4,000 characters is refused. Retry on 429 and 5xx with the same key.
Reading Starlings as the person who asked
On /mcp the agent reads as the member who addressed it, never with more access than that member has. An enrolled agent only reads. Sign the JSON-RPC body with inbound_key over <timestamp>.<agent_email>.<actor_email>.<body>, and send x-meet-agent: <agent_email>, x-meet-actor: <actor_email>, x-internal-key-id: <agent_email>, with the timestamp and signature headers.
The manifest
Starlings reads GET <endpoint origin>/manifest and shows it on the agent's card in Team. Return JSON with a description, and optionally model, skills, tools, connections, schedules, home_url, repo_url and version. Never put a secret in it.
Stuck turns
If the agent enrolled a reset_endpoint and accepts a hand-off without answering, the watchdog posts {"thread_id": "..."} to it, signed with webhook_secret, so the agent can drop that conversation's run, and tells the person in the conversation. Without a reset_endpoint, Working… clears when its time runs out.
Control
- An admin Freezes an agent in Admin › Agents: it gets nothing and can send nothing from its next call. Unfreeze lets it back.
- An admin Removes an enrolled agent: its keys stop working at once and its conversations stay. Its address is never given to another agent.
- Admin › Agents shows each agent's hand-offs, replies, delivery problems and watchdog resets.
Where it works
| Capability | macOS Host | macOS Join | Web | iOS / iPadOS | Agent access |
|---|---|---|---|---|---|
| Any agent joins Team by enrollment | Not available | Not available | Works | Not available | /api/agents/enroll, /api/agents/enroll/poll |
Verified · docs/help/howto/connect-an-agent-as-a-team-member.md