Skip to content

Envoys

Envoys

An envoy is an agent role for outside senders on a conversation instance — WhatsApp, Telegram, email, Slack, Discord or WeChat. It is declared in src/agent/envoy/+<id>.envoy.ts and answers on every instance whose envoy names it.

// src/agent/envoy/+field_ops_whatsapp.envoy.ts (templates/field-operations, excerpt)
import { envoy } from '@norbital-ai/bolt';

export default envoy({
  name: 'Norbital',
  audience: 'private',
  policies: ['field_ops_whatsapp'],
  groupMessages: 'mention_or_reply',
  delegation: 'disabled',
  triage: {},
  task: 'You answer questions about job assignments on WhatsApp and keep them up to date from what people report. …'
});

// src/+workspace.ts (excerpt): the WhatsApp instance it answers on
connections: {
  field_ops_whatsapp: { integration: 'whatsapp', envoy: 'field_ops_whatsapp' }
}

The declaration

  • name — what people call it: a message that says it is addressed to it, mention or not
  • audience — 'public' answers anyone; 'private' answers only senders whose handle is registered on a member (mobile number on WhatsApp, email address on email, Telegram handle on Telegram), and sends others one fixed not-recognised reply
  • policies — required and non-empty: the authority of every turn, and the source of its limits
  • groupMessages — 'mention_or_reply' (the default), 'disabled' or 'all'
  • delegation — 'enabled' lets it hand work to sub-agents under the same authority
  • triage — false opts out; otherwise { scope?: 'dm' | 'group' | 'all' }, defaulting to 'all'. When the host provides AI, triage decides whether to respond, wait or leave a group message ambient; mentions and replies bypass it
  • dm — a linked member’s direct message: 'union' (the default) joins their own authority, 'envoy' keeps the envoy’s alone
  • task — a standing directive on top of the brief, never who may do what

Whose authority a turn holds

  • A direct message from a sender linked to a member runs under the envoy’s policies joined with that member’s own authority; with dm: 'envoy' it runs under the envoy’s policies alone, their operands bound to the member, never as an administrator. A direct message from an unlinked sender runs under the envoy’s policies alone on a public envoy, and is not admitted on a private one: one fixed not-recognised reply, no turn. An administrator sets a member’s mobile number and Telegram handle in Settings → People; there is no registration.
  • A group turn runs under the envoy’s policies alone, whoever sent the message; the sender is named in the message header, so the model knows who asked.
  • An envoy is never an administrator by itself, and its limits always come from its own policies.
// src/access_control/policy/+field_ops_whatsapp.policy.ts (excerpt)
import { policy } from '@norbital-ai/bolt';

export default policy({
  grants: {
    job_assignments: {
      read: true,
      update: {
        fields: ['status', 'completed_at', 'summary', 'photo_evidence', 'communication_logs']
      }
    },
    photo_evidence: {
      create: { fields: ['job_assignment_id', 'photo', 'source'] }
    },
    communication_logs: {
      create: { fields: ['job_assignment_id', 'message', 'sent_at', 'sender', 'source_message_id'] }
    },
    sys_user: { read: true, fields: ['id', 'name'] }
  },
  limits: {
    act: '60/min',
    'envoys.receive': [
      { rate: '30/min', per: 'sender' },
      { rate: '300/min', per: 'subject' }
    ]
  }
});

Groups

There is one conversation per group chat. With mention_or_reply, a mention or reply starts a turn directly. Other messages may pass through triage when it covers groups: the agent can respond, wait or ignore them. With triage: false or triage: { scope: 'dm' }, unaddressed group messages stay ambient — kept in history and readable with read_messages, without starting a turn. groupMessages: 'disabled' disables group turns, including triage. A message admitted mid-turn joins the running turn at its next step. Email is always addressed and bypasses triage.

Writes in a conversation

A conversation has no confirmation card, so an envoy’s writes execute directly and each outcome is appended to the reply as a receipt; a held write is reported as awaiting approval. An inbound attachment is a file the envoy can read with read_attachment and file into a file field.

Where it answers

An envoy is deployed by an instance, never by its own declaration: a +workspace.ts connection names it (connections: { field_ops_whatsapp: { integration: 'whatsapp', envoy: 'field_ops_whatsapp' } }), or an administrator selects its Envoy in an integration’s Configuration tab. One instance holds at most one envoy; one role may answer on several instances, each with its own card under Agent channels. Pausing an instance silences the envoy on that account. A personal instance (one a member owns) never deploys an envoy.

The filename supplies the role id (field_ops_whatsapp); name: 'Norbital' supplies its display name. The connection slot supplies the instance id. These may have the same spelling, but the role and instance are separate. Publish the role, its policies and the connection together in a workspace release. The named instance is created from source; pair it at run time. sys_integration.envoy selects its role, and sys_integration.active controls whether it serves. Credentials and connection status belong to that instance. Do not seed a named instance, an envoy deployment or credentials. See Integrations.

Connect and configure in the workspace

  1. Sign in as a workspace administrator and open More → System → Integrations (/settings/integrations). Under Agent channels, find the declared display name. Field Operations shows Norbital · Private with a WhatsApp card for its existing field_ops_whatsapp connection. If already Connected, use Manage; otherwise review Import start and pair it below. When a role has no connection yet, Connect creates a workspace instance and assigns the role to it.
  2. When offered, choose Import start before Connect: From now saves the host’s current instant; Since a chosen date starts at midnight on the Start date in the supplied IANA Time zone; All available history allows the history the provider can supply. With a cutoff, older messages and messages without a source timestamp are skipped. Review the displayed choice: existing connections retain their saved setting, or use All available history when none is saved. After connecting, change it under Manage → Connection → Import start → Apply import start. Changes affect future imports; messages already stored remain.
  3. Follow the provider’s setup. If asked to choose a provider, select Unofficial (WhatsApp Web). Use a dedicated WhatsApp account; WhatsApp may restrict accounts it detects automating. If the form shows Phone number (optional), leave it blank and click Connect for a QR, or enter digits with the country code first and click Connect for a pairing code. On the phone, open WhatsApp → Settings → Linked devices → Link a device to scan the QR or enter the code. When a QR is already showing, Use a phone number instead opens the code-pairing form. Wait for Connected and check the account shown.
  4. Click Manage on the agent’s channel card to open its detail sheet, then use Connection → Test connection when offered. WhatsApp Web sends the test to the connected account itself; check “Message yourself” on that phone. This checks transport delivery. The inbound report below checks member admission, the agent and record writes.
  5. In Manage → Configuration, verify Envoy is Norbital and Active is enabled, then Save any change. The template supplies this role by default; runtime changes are preserved. Manage remains available for an existing connection while unpaired or paused. Use this channel card for the agent connection; Other connections holds the remaining external accounts and is titled Connections when the workspace declares no agent roles. A workspace conversation instance has no member owner. Extra configuration fields appear only when the integration definition declares them; Field Operations uses the built-in WhatsApp definition and needs no extra configuration values. Pairing secrets go in the provider setup, not in the configuration JSON.
  6. In More → Settings → People, set each sender’s mobile number, with country code, on their active workspace account. Give contractors the Contractor team for their app access. The private WhatsApp envoy matches the sender’s number to the account; the WhatsApp display name and the text of a message do not establish identity. There is no separate envoy registration link.

Other conversation channels use the same instance setup when offered by the host. Telegram bot asks for the token from @BotFather → /newbot; the host registers its webhook, and a test needs a chat id after you first message the bot. Email offers the host’s mailbox providers: complete the Google sign-in window or follow the selected provider’s form. For private roles, populate the member’s Telegram handle or email address respectively. Channels without a member handle, including the built-in Slack, Discord and WeChat conversations, cannot admit senders to a private envoy. Changing the transport also requires a task and policies suitable for that channel; the Field Operations task explicitly describes WhatsApp.

Verify the Field Operations flow

The example policy reads every job assignment and allows status, completion, summary, new evidence and new communication rows on any assignment. It is not restricted to the sender’s assigned jobs. The envoy policy grants no assignment creation, deletion, reassignment, work-order changes, evidence or log reads, or suspicion-review access. A linked member’s DM also joins their own grants by default; group turns use the envoy policy alone. Adjust the source policy and dm setting if your workflow needs a different boundary.

  1. Have a controller create and assign an existing job. From a different, linked contractor number, send a DM to the connected WhatsApp account, naming the site and work clearly: “Grab bars at [site] are installed. Work complete; photo attached.” Attach a job photo. If several assignments match, the envoy should ask which one. It does not create a work order from the report.
  2. The envoy searches for the assignment and submits one job_assignments.update: progress and summary, nested photo_evidence.create for each photo, and nested communication_logs.create for the report’s text. It confirms after the update succeeds. Open that assignment in the controller or contractor app and check the completed status, summary, photo and communication entry.
  3. Each photo records the WhatsApp conversation, message, attachment, sender and time as its source; each communication row records its source message id. Photo inspection and the suspicion review run separately after filing. Inspect those outcomes as a controller; a successful envoy receipt does not mean the evidence has passed review.
  4. Check admission with read-only questions: a linked number can ask about an existing job; an unknown number’s DM receives the fixed not-recognised reply without a model turn. In a group containing the connected account, a linked sender’s mention or reply starts a turn directly. The source’s triage: {} also covers unaddressed group messages from linked senders: the agent may respond, wait or leave them ambient. To require a mention or reply for every group turn, change the source to triage: false or triage: { scope: 'dm' }.

Pause, reconnect and diagnose

  • Pause: open Agent channels → Manage → Configuration, turn Active off and save. The channel card shows Paused and the envoy stops answering. Open Manage again, enable Active and save to resume with the stored pairing.
  • Reconnect on the Connection tab opens provider setup again. Disconnect unpairs the external account and removes its stored credentials. On the Agent channels card it also deletes an instance created there; a named +workspace.ts connection remains, unpaired, for the next Connect. Use pause when you only need to stop replies temporarily.
  • Not available on this host means the host does not offer an available provider for that channel. A source declaration does not configure host providers or their OAuth applications. If the role is missing, check that the active workspace release includes the envoy and policy files. If pairing fails, follow the provider’s displayed error and retry setup.
  • When a connected envoy does not answer, check Active, the selected Envoy, the sender’s active member record and number, group addressing, and rate limits. Open the channel card’s Manage → Messages → Refresh to inspect inbound messages, outbound delivery states and errors. A transport test passing does not prove an inbound message has the authority to update a record.

Limits

The envoy’s policies carry envoys.receive (per sender and per envoy) and envoys.unrecognised (one not-recognised reply per 15 minutes per sender by default), beside the act, read and agent budgets that bound the whole desk.