Skip to content

Conversation Channels

Conversation channels let people reach vornik from the tools they already use. Connect a channel and inbound messages turn into tasks or chat turns, while vornik's replies flow back out the same way they came in. vornik ships four channels: GitHub, Slack, email, and voice messages (experimental).

You configure each channel per project, in that project's YAML file. After you change a channel's settings, reload the project configuration — you do not need to take the whole service down to pick up new channels.

Things every channel has in common

  • Secrets never live in YAML. Tokens, passwords, and signing secrets are supplied through environment variables. The YAML only names the environment variable to read (for example bot_token_env), so your config files stay safe to commit.
  • Allowlists are your front door. Each channel has a sender allowlist and, where it applies, a channel/repo allowlist. Leaving an allowlist empty means "accept everyone" — fine while you are testing, but you should tighten it before going to production.
  • Routing is automatic. A single channel connection can serve several projects when the upstream service gives a stable identifier (a GitHub installation, a Slack workspace, an email login). vornik routes each inbound message to the right project for you.

Tip

Start every channel with an allowlist locked down to just the people and places you expect. Loosen it once you have confirmed the flow works.


GitHub

The GitHub channel lets vornik respond to issues and pull requests in the repositories you choose. It can:

  • Open a task when an issue gets a label you have nominated.
  • Open a review task when a pull request is opened.
  • Reply when someone mentions @vornik on an issue or pull request.

Setup

  1. Create a GitHub App in your organization's settings.
  2. Point the App's webhook URL at your vornik host's GitHub webhook endpoint (/api/v1/github-app/webhook).
  3. Generate a webhook secret, download the App's private key, and note the App ID and installation ID.
  4. Subscribe the App to issue, issue-comment, pull-request, and pull-request-review-comment events.
  5. Grant the installation read and write access to issues and pull requests on every repository you want vornik to handle.

Configuration

github_app:
  app_id: 1234567
  private_key_path: /path/to/github-app.pem
  installation_id: 89012345
  webhook_secret_env: VORNIK_GITHUB_APP_WEBHOOK_SECRET
  repo_allowlist:
    - "your-org/your-repo"
  task_labels:        # labels that create a task (the label name becomes the task type)
    - "vornik"
    - "research"
  pr_review_labels:   # leave empty to review every opened PR
    - "needs-review"
  sender_allowlist:   # GitHub login names allowed to trigger vornik
    - "your-github-login"

Supply the webhook secret in the environment before starting vornik:

export VORNIK_GITHUB_APP_WEBHOOK_SECRET="your-webhook-secret"

A webhook secret and a non-empty repo_allowlist are the minimum for vornik to receive events. To let it reply, also set app_id, private_key_path, and installation_id.

Good to know

  • Label matching is case-sensitive — GitHub keeps your label's exact casing, so Vornik and vornik are different.
  • An empty pr_review_labels means every opened pull request creates a review task. List specific labels to require at least one of them.
  • sender_allowlist matches GitHub usernames, including bots. If you want a bot to be able to trigger vornik, add it explicitly.
  • If you are testing by re-sending a webhook, change the delivery so it looks new — vornik deduplicates redelivered events so it never double-comments.

Slack

The Slack channel lets vornik answer in direct messages, when it is @-mentioned in a channel, or when it is mentioned in a public channel it has been allowed into.

Setup

  1. Create a Slack App at https://api.slack.com/apps.
  2. Enable the Events API and set the request URL to your vornik host's Slack webhook endpoint (/api/v1/slack/webhook). vornik answers Slack's verification challenge automatically the first time.
  3. Subscribe the bot to the app_mention, message.im, and message.channels events.
  4. Under OAuth & Permissions, add the app_mentions:read, chat:write, im:history, im:read, channels:history, files:read, files:write, and commands scopes. files:read lets vornik consume inbound attachments; files:write lets it return reports and other task artifacts to Slack.
  5. Under Slash Commands, create /vornik and set its request URL to the same /api/v1/slack/webhook endpoint. Use /vornik <prompt> to start or continue the per-user channel session.
  6. Install (or reinstall) the App to your workspace and capture the signing secret and the bot token (it starts with xoxb-).

Configuration

slack:
  team_id: "T01234567"
  signing_secret_env: VORNIK_SLACK_SIGNING_SECRET
  bot_token_env: VORNIK_SLACK_BOT_TOKEN
  channel_allowlist:
    - "C0ABCDEF12"
  sender_allowlist:
    - "U0AAAAAAA"
  # Optional. The command this deployment answers; defaults to /vornik. Set it
  # when the bot is branded differently, and match the app's own manifest.
  slash_command: "/vornik"
  # Optional. A turn can take a minute, and Slack gives bots no typing
  # indicator, so vornik posts a short "working on it" placeholder and replaces
  # it with the answer. Set false to keep the channel silent until the reply.
  progress_signal: true
  # Optional. How long a turn may run before that placeholder appears.
  progress_signal_gap: "2s"

Supply the secrets in the environment before starting vornik:

export VORNIK_SLACK_SIGNING_SECRET="your-signing-secret"
export VORNIK_SLACK_BOT_TOKEN="xoxb-..."

A signing secret and team_id are the minimum to receive events; the bot token is what lets vornik reply.

Good to know

  • An empty channel_allowlist or sender_allowlist denies everything (direct messages excepted from channel_allowlist — see below). A Slack bot is addressable by every member of the workspace it is installed in, so an unconfigured allowlist would otherwise mean the whole workspace could drive the project — spending its budget and reaching its tools — with nothing in the config saying so. List the channels and senders you intend, or set allow_unlisted_senders: true to opt into the open behaviour deliberately (dev only).
  • Slack rejects requests whose timestamps are more than five minutes off, a standard replay defense. If valid messages get rejected, check that your server's clock is in sync.
  • Slack limits how fast a bot can post (roughly one message per second per channel). vornik respects this automatically and waits when Slack asks it to.
  • Slack retries Events API deliveries when acknowledgement is slow. vornik drops Slack-marked retries (including when they reach another replica) and deduplicates repeated event_id values, so one message produces one dispatcher turn.
  • /vornik is the Slack command entry point. Telegram-specific commands such as /tasks are not separately registered in Slack; ask for those operations through /vornik, for example /vornik show my running tasks.
  • Slash commands use a per-user, per-channel session separate from ordinary message threads. Their delayed answers post at channel level because a slash invocation has no Slack message timestamp to reply under.
  • Direct messages are never gated by channel_allowlist, whether it lists channels or is left empty. A DM's channel id is created by Slack the first time each person writes to the bot, so it cannot be listed in advance. sender_allowlist is the control for DMs — so a DM-only bot lists its senders and leaves channel_allowlist empty, which denies every real channel while direct messages keep working. This covers one-to-one DMs only: a group DM is not a DM for this purpose, and vornik does not support group DMs at all — the bot is not subscribed to group-DM history, and a group-DM message is dropped as an unhandled conversation type. Adding the bot to a group DM produces silence, not an answer.
  • In a thread, you only tag the bot once. Replies inside a thread vornik is already part of continue the conversation without a mention. A new top-level message in a channel still needs an @-mention, which is what keeps the bot out of conversations between colleagues.
  • Images posted to Slack are read: they go to the model directly when it can see images, and to the vision role when it cannot. Voice memos are transcribed when voice is configured.
  • Reports and other operator-facing task outputs can be uploaded back into the originating Slack thread. This requires the files:write OAuth scope; after adding it to an existing app, reinstall the app to the workspace.
  • Opening the app's Home tab shows what this deployment serves and how to reach it. Subscribe to the app_home_opened bot event to enable it; no extra OAuth scope is needed.
  • You can see what a turn is doing. While vornik works, the "working on it" message is rewritten in place to name the current step — searching memory, scheduling the job, calling an external service. It is replaced by the answer when the turn finishes, and it never becomes part of the conversation.
  • A job scheduled from chat reports back. When a task vornik created for you finishes, it posts the outcome and a link into the same conversation, whether or not it succeeded. That works across a daemon restart: the link between the task and your conversation is stored in the database, not in memory.

Reporting a problem with vornik

Ask the bot to report a bug ("something is broken, please file a report") on any channel and it prepares an anonymised report: hostnames, file paths, IP addresses, email addresses and anything resembling a credential are stripped automatically.

It gives you back a link, and does not submit anything itself. The report would go to a public repository, and automatic redaction cannot know whether your own description mentions a customer or a system you would rather not name — so you read it on GitHub and press submit yourself. The same rule applies to vornikctl report on the host.


Email

The email channel reads inbound mail over IMAP and replies over SMTP, so anyone can interact with vornik just by sending it a message. It handles multipart and HTML mail, decodes international character sets, and saves attachments as task artifacts.

Setup

Bring your own IMAP and SMTP service. The channel works with common providers. For Gmail, for example:

  1. Turn on two-factor authentication for the account.
  2. Create an App Password and use it for both IMAP and SMTP.
  3. Use imap.gmail.com:993 (TLS) and smtp.gmail.com:587 (STARTTLS).
  4. If you want vornik to read from a folder other than INBOX, create it first and set imap_mailbox.

Configuration

email:
  imap_host: imap.gmail.com
  imap_port: 993
  imap_username: [email protected]
  imap_password_env: VORNIK_ASSISTANT_IMAP_PASSWORD
  imap_mailbox: "INBOX"
  smtp_host: smtp.gmail.com
  smtp_port: 587
  smtp_username: [email protected]
  smtp_password_env: VORNIK_ASSISTANT_SMTP_PASSWORD
  from_address: "Assistant <[email protected]>"
  sender_allowlist:
    - "[email protected]"
    - "example.com"            # bare domains are allowed too
  poll_interval: "60s"
  attachment_size_cap_bytes: 26214400   # 25 MiB
  attachment_store_dir: /path/to/email-attachments

Supply the credentials in the environment before starting vornik:

export VORNIK_ASSISTANT_IMAP_PASSWORD="your-imap-password"
export VORNIK_ASSISTANT_SMTP_PASSWORD="your-smtp-password"

The IMAP host, username, and password are the minimum to receive mail. Add the SMTP block and from_address to let vornik reply; an inbound-only setup is valid if you only want it to act on incoming mail.

Good to know

  • sender_allowlist accepts full addresses ([email protected]) and bare domains (example.com). An empty allowlist accepts everyone — tighten it in production.
  • Set attachment_size_cap_bytes to a sensible limit. Leaving it unset means unlimited, so a single huge attachment could land in your artifact store. Matching your mail provider's per-message cap (Gmail's is 25 MiB) is a good default.
  • Set attachment_store_dir the first time you enable the channel. If it is empty, attachment contents are silently dropped.
  • The channel polls on poll_interval (60 seconds by default). Very short intervals are accepted but discouraged.
  • Optional inbound authentication checking (verify_inbound_auth) relies on your mail server stamping SPF/DKIM results. If you turn it on, start with the relaxed policy and confirm legitimate mail passes before moving to strict.

Voice messages

Experimental

Voice messages are an experimental feature. The transcription/synthesis pipeline and its configuration may change between releases, and quality depends on your chosen STT/TTS models. Use it for evaluation rather than production-critical conversations for now.

The voice channel adds spoken conversation to Telegram and Slack. When someone sends a voice message, vornik transcribes it, treats the transcript as a normal message, and — if the conversation started in voice — speaks its reply back.

A conversation's "voice mode" is sticky: once someone speaks, vornik keeps replying with audio until the next message comes in as text.

The current release uses local, open-weight speech engines:

  • Speech-to-text: whisper.cpp (whisper-cli) with a Whisper ggml model file.
  • Text-to-speech: Piper with a Piper voice.
  • Audio conversion: ffmpeg, with Opus for Telegram and AAC for Slack.

Since 2026.9.7 the programs ship in the agent image and every voice note is processed there, in a one-shot container with no network and a memory, CPU and time limit (sandbox_tools.limits.voice_stt / voice_tts, see Artifacts and delivery). Nothing is installed on, or run on, the daemon host. You download only the model files; vornik mounts each model's directory read-only into the container.

Configuration

voice:
  stt:
    provider: "whisper-local"
    model: "/path/to/ggml-base.en.bin"
    language_hint: "en"        # leave empty for English
  tts:
    provider: "piper"
    voice: "/path/to/en_US-amy-medium.onnx"
    speed: 1.0
    max_text_runes: 1500

You can configure stt alone (vornik transcribes incoming audio and replies with text) or tts alone (vornik reads its replies aloud) — you do not need both.

Good to know

  • max_text_runes caps how much text gets spoken (1500 is about 90 seconds). Telegram limits voice messages to 60 seconds, so set this to suit your channel; vornik trims overly long replies rather than failing silently.
  • binary_path and ffmpeg_path are ignored since 2026.9.7: the programs come from the agent image. The daemon logs a warning while they are still set; remove them. An agent image older than the release lacks the tools, and voice then reports "not available in the agent image" — there is no fallback to a program on the host.
  • Run vornikctl doctor: its sandbox_tools check transcribes a one-second clip with your Whisper model, synthesises a phrase with your Piper voice, and round-trips Opus and AAC through ffmpeg, all in the sandbox, so a wrong or missing model shows up there rather than at the first voice note.
  • Use the smallest Whisper model that is accurate enough for you. On a machine without a GPU, larger models can take many seconds to transcribe a clip.
  • For Piper, point voice at the .onnx file; its matching .onnx.json must sit beside it (the whole directory is mounted).
  • voice.stt.model also serves audio document extraction unless extractors.audio.model_path names a different model.

A note on history and replies

Conversation history belongs to the conversation, not to the channel, so vornik remembers context across messages even if a channel reconnects. vornik sends one complete reply per turn rather than streaming it word by word (live typing indicators are specific to the Telegram and web chat experiences).

For tuning the model behavior and safety controls that sit behind every reply, see Workflows and LLM controls.