Each key is an event name; the value is a list of channel names declared in `[channels.*]`. An empty list or absent key means no notification for that event.
### Workflow lifecycle notifications
Per-workflow notification targets are set in the workflow template:
```toml
[[workflows]]
name = "nightly_analysis"
trigger = "schedule"
[workflows.nightly_analysis.notifications]
on_stage_complete = ["team-slack"]
on_stage_failed = ["team-slack", "ops-discord"]
on_completed = ["team-slack"]
on_cancelled = ["ops-discord"]
```
## Secret Resolution
Token values in `[channels.*]` blocks are interpolated from the environment before any network call is made. Two syntaxes are supported:
| Syntax | Behaviour |
|--------|-----------|
| `"${VAR}"` | Replaced with `$VAR`; startup fails if the variable is unset |
| `"${VAR:-default}"` | Replaced with `$VAR` if set, otherwise `default` |
Resolution happens inside `ChannelRegistry::from_config` — the single mandatory call site. There is no way to construct a registry with an unresolved placeholder.
If a required variable is absent and no default is provided, VAPORA exits at startup with:
```text
Error: Secret reference '${SLACK_WEBHOOK_URL}' not resolved: env var not set and no default provided
```
## Supported Channel Types
### Slack
Uses the [Incoming Webhooks](https://api.slack.com/messaging/webhooks) API. The webhook URL is obtained from Slack's app configuration.
```toml
[channels.my-slack]
type = "slack"
webhook_url = "${SLACK_WEBHOOK_URL}"
```
Payload format: `{ "text": "**Title**\nBody" }`. No SDK dependency.
### Discord
Uses the [Discord Webhook](https://discord.com/developers/docs/resources/webhook) endpoint. The webhook URL includes the token — obtain it from the channel's Integrations settings.
Uses the [Bot API](https://core.telegram.org/bots/api#sendmessage) `sendMessage` endpoint. Requires a bot token from `@BotFather` and the numeric chat ID of the target group or channel.
Two endpoints are available under `/api/v1/channels`:
### List channels
```http
GET /api/v1/channels
```
Returns the names of all registered channels (sorted alphabetically). Returns an empty list when no channels are configured.
**Response**:
```json
{
"channels": ["ops-discord", "team-slack"]
}
```
### Test a channel
```http
POST /api/v1/channels/:name/test
```
Sends a connectivity test message to the named channel and returns synchronously.
| Status | Meaning |
|--------|---------|
| `200 OK` | Message delivered successfully |
| `404 Not Found` | Channel name unknown or no channels configured |
| `502 Bad Gateway` | Delivery attempt failed at the remote platform |
**Example**:
```bash
curl -X POST http://localhost:8001/api/v1/channels/team-slack/test
```
Expected Slack message: `Test notification — Connectivity test from VAPORA backend for channel 'team-slack'`
## Delivery Semantics
Delivery is **fire-and-forget**: `AppState::notify` spawns a background Tokio task and returns immediately. The API response does not wait for webhook delivery to complete.
Behaviour on failure:
- Unknown channel name: `warn!` log, delivery to other targets continues
- HTTP error from the remote platform: `warn!` log, delivery to other targets continues
- No channels configured (`channel_registry = None`): silent no-op
There is no built-in retry. A channel that is consistently unreachable produces `warn!` log lines but no escalation. Use the `/test` endpoint to confirm connectivity after configuration changes.
## Events Reference
| Event key | Trigger | Default level |
|-----------|---------|---------------|
| `on_task_done` | Task moved to `Done` status | `Success` |
| `on_proposal_approved` | Proposal approved via API | `Success` |
| `on_proposal_rejected` | Proposal rejected via API | `Warning` |
The webhook URL must end with `/slack` for Slack-compatible mode, or be the raw Discord webhook URL. Ensure the URL copied from Discord's channel settings is used without modification.
### Telegram chat_id not found
The bot must be a member of the target group or channel. For groups, prefix the numeric ID with `-` (e.g. `-1001234567890`). Use `@userinfobot` in Telegram to retrieve your chat ID.
## Related Documentation
- [Workflow Orchestrator](./workflow-orchestrator.md) — workflow lifecycle events and notification config