Skip to content

Messages between agents and people

An agent can send an update, ask a question, request an approval or report a result. Recipients read and answer in Astrolift, the CLI or another client using their own Astrolift identity. The client connects to Astrolift; you do not need to expose an inbound endpoint on your computer.

Use completion callbacks when a service needs a signed final-task event. The inbox is for conversations with people.

Check access and open the inbox

astro status --json
astro inbox --help

The installation must advertise agents.messages. The CLI must include the inbox commands. Open Inbox in the dashboard to read threads and answer inline. CLI clients can list messages, show a message, reply and follow new messages. Check each command's help for the flags in your installed version.

astro inbox list --unread --limit 25 --json
astro inbox show <message-guid> --mark-read
astro inbox reply <message-guid> --choice <option-id> --request-id <request-uuid>
astro inbox follow --interval 5s --json

Use --thread <thread-guid> to narrow a list or follow to one thread. list supports --after <cursor> for another page. show looks up the exact message GUID; it does not substitute an agent slug or a different message.

For text, reply --stdin reads the body from standard input; --body is also available. Reply text is limited to 4,000 Unicode characters. Repeat --choice for multiple option IDs, or use --decision allow or --decision deny for an approval. Keep the printed request UUID when a reply's result is uncertain and reuse it with --request-id for recovery. The command reports the server's recorded route, not completion of the agent's next action.

follow polls the GraphQL inbox and emits JSON Lines with --json. It deduplicates message IDs, bounds each catch-up read and retries transient transport failures with backoff. Authentication or authorization errors stop the command. It does not require an inbound callback or claim a WebSocket connection.

Receiving a message does not grant access to its agent. The server checks the recipient's current agent access and the credential's scope. Losing access stops further reads and replies, including within a thread you previously read.

Read messages through the API

query AgentMessages($threadId: GUID, $after: String) {
  astroliftAgentMessages(threadId: $threadId, after: $after, first: 50) {
    nextCursor
    totalCount
    unreadCount
    items {
      id threadId kind source title body
      agentSlug agentName taskId taskStatus
      createdAt deliveredAt readAt expiresAt answerable
      options { id label description }
      attachments { kind ref label }
      replies { id authorLabel body choices decision route deliveredAt wakeTaskId }
    }
  }
}

Messages are returned newest first. Follow nextCursor to read more pages. unreadOnly: true filters the message list. Use astroliftAgentMessageThreads for the thread list; it returns each thread's latest message, message count and unread count.

Clients can listen to astroliftAgentMessageReceived on the existing GraphQL subscription transport, then refresh the affected thread. A subscription checks current access throughout the connection. Reconnecting clients should refresh their inbox and deduplicate by message ID.

The WebSocket endpoint is wss://<install>/app/gql/config/ws/, using the graphql-transport-ws subprotocol. Send connection_init with {"authorization":"Bearer <user-token>"} as its payload, wait for connection_ack, then send a subscribe message containing the subscription query. The token supplies the organization and scope ceiling. Do not put it in the URL. A presented invalid bearer is refused rather than falling back to a browser cookie. The subscription covers new messages; use the paged queries for earlier history.

Mark only the messages or thread you actually read:

mutation MarkRead($ids: [GUID!], $threadId: GUID) {
  markAgentMessagesRead(ids: $ids, threadId: $threadId) {
    ok
    errors { code message field }
    data { marked }
  }
}

Omitting both selectors marks all accessible messages read. An unread count is separate from whether an agent received or acted on a reply.

Answer a question or approval

mutation Reply($input: AstroliftReplyToAgentMessageInput!) {
  replyToAgentMessage(input: $input) {
    ok
    errors { code message field }
    data {
      id messageId body choices decision route
      deliveredAt wakeTaskId createdAt
    }
  }
}

Pass the exact messageId, text in body, option IDs in choices, or allow or deny in decision for an approval. Reuse one clientRequestId when recovering an uncertain reply submission. Check answerable, the current message and the server's response before assuming the agent can proceed.

Replies to a run's input or approval request go through the existing gate. An approval requires the permission to send input to that run and follows the configured action-safety policy. A read-only token cannot approve an action merely because its owner has broader permissions.

For an active run, a reply can be delivered through its input channel. A reply to an ended run may wake a new run when the person has dispatch permission, or remain queued for the agent to collect. Read the reply's route, deliveredAt and wakeTaskId; accepting the reply is separate from the agent completing another action.

Let an agent send messages

Astrolift supplies the workload's message and run-MCP endpoints as ASTROLIFT_MESSAGES_URL and ASTROLIFT_RUN_MCP_URL. These endpoints use the run's own callback identity. A person's API token or a dispatcher key does not become that run's identity.

The run-scoped MCP server exposes astrolift_message_user and astrolift_get_message_replies. Keep its credentials inside the workload. Do not paste them into messages or give them to a desktop client. A person uses the GraphQL inbox with their own token.

An agent may address only recipients who can read it. An unknown or disallowed recipient refuses the send. Questions can carry option IDs and an expiry; attachments are references to supported artifacts, installation URLs or semantic-layer citations. Keep message bodies concise and avoid secrets. Redaction on the send path is a backstop, not a reason to include credentials.

An attachment has kind, ref and an optional label. Supported kinds are artifact, citation and url. URL references must be installation-relative paths or HTTPS URLs on the installation's configured host. A message accepts up to ten attachments; each reference is at most 1,000 characters and its label at most 200. A reference alone does not prove that the reader can retrieve its content; resolve it through the corresponding authorized resource API.

Notification channels and retention

In-app delivery and read state belong to each recipient. Existing notification preferences control available push and email fan-out; the installation must also have those channels configured. Webhook subscriptions can select the agent.message.sent event. A channel accepting a notification does not prove that the recipient read it.

When the installation advertises notifications.quiet_hours, a person can set quiet hours for agent-message push and email in notification settings. Messages remain in the inbox; the server records suppressed channel deliveries. Quiet hours do not queue a later digest or mute unrelated events such as app-expiry warnings. Those events retain their own notification preferences.

query MyQuietHours {
  astroliftMyNotificationQuietHours {
    enabled timezone startMinute endMinute active
  }
}

mutation SetMyQuietHours($input: SetNotificationQuietHoursInput!) {
  setNotificationQuietHours(input: $input) {
    ok
    errors { code message field }
    data { enabled timezone startMinute endMinute active }
  }
}

The settings are personal and authenticated. For 22:00 to 08:00 in Costa Rica, pass enabled: true, timezone: "America/Costa_Rica", startMinute: 1320 and endMinute: 480. Minutes must be distinct integers from 0 to 1439. The start is included and the end excluded; windows may cross midnight. The server uses the chosen IANA timezone, including daylight-saving transitions, to return active. Defaults are disabled with UTC 22:00 to 08:00.

The purge command uses the organization's retention policy. Operators can run manage.py purge_agent_messages to purge expired history and should include it in their maintenance schedule. The command's availability does not establish that a periodic purge has already been installed.