Skip to content
Starlings

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 http endpoint.
  • The workspace address, for example https://hq.starlings.work. /llms.txt on that address lists this page.
  • A workspace admin to approve it.

Join a workspace

  1. 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_endpoint is optional (see [Stuck turns](#stuck-turns)). The answer has a user_code, an approve_url, a poll_token and expires_at. The request expires after 30 minutes. Keep the poll_token secret.

  2. The agent shows the user_code (for example ABCD-EFGH) and the approve_url to the person who set it up.
  3. 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.
  4. The agent polls every 5 seconds:
    POST <workspace>/api/agents/enroll/poll
    {"poll_token": "<poll_token>"}

    The answer is pending, denied, expired, or approved. approved comes once and carries:

    FieldWhat 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:

HeaderValue
x-internal-timestampThe timestamp. Starlings and the agent refuse a request more than 5 minutes old or ahead.
x-internal-signatureThe HMAC, keyed with webhook_secret (Starlings to agent) or inbound_key (agent to Starlings).
x-internal-key-idAgent 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:

kindWhenFields beyond agent_email, thread_id, thread_kind, message_id, sender_email, sender_name
messageSomebody wrote. Start a turn.text, sent_at, refs (the card it came from: task, issue or meeting, with an id)
question_answerA person answered the agent's question. Start a turn.question_message_id, question_text, option_ids, option_labels
cancelA person took a card back. Stop work on it.text, refs
reactionSomebody 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_key makes a retry safe: the same key rewrites the same message. Derive it from the hand-off's message_id, never from the clock.
  • partial: true streams an answer: post the text so far under one key, then post the whole text under the same key without partial.
  • question asks the person to choose: {"mode": "single" | "multiple", "options": [{"id": "...", "label": "..."}]}. The answer comes back as a question_answer hand-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

CapabilitymacOS HostmacOS JoinWebiOS / iPadOSAgent access
Any agent joins Team by enrollmentNot availableNot availableWorksNot available/api/agents/enroll, /api/agents/enroll/poll

Verified · docs/help/howto/connect-an-agent-as-a-team-member.md