The Healer sends notifications through multiple channels whenever a ticket changes state. Webhooks are the primary way to stay informed about self-healing activity without watching the console. Click a link in Discord, review the proposed fix in your browser, click Approve.
| Platform | URL Pattern | Payload Format |
|---|---|---|
| Discord | discord.com/api/webhooks/... |
{"content": "formatted message"} |
| Slack | hooks.slack.com/services/... |
{"text": "formatted message"} |
| Microsoft Teams | webhook.office.com/... or outlook.office.com/... |
{"text": "formatted message"} |
| Raw JSON | Anything else | Full JSON payload (all fields) |
Platform is auto-detected from the webhook URL. No configuration needed beyond the URL itself.
export SPECORA_HEALER_WEBHOOK_URL="https://discord.com/api/webhooks/123456789/abcdef..."
Send notifications to multiple platforms simultaneously using comma-separated URLs:
export SPECORA_HEALER_WEBHOOK_URL="https://discord.com/api/webhooks/123/abc,https://hooks.slack.com/services/T00/B00/xxx"
Every notification goes to every URL in the list.
from healer.notifier import Notifier
# Single channel
notifier = Notifier(webhook_url="https://discord.com/api/webhooks/123/abc")
# Multi-channel
notifier = Notifier(webhook_url="https://discord.com/api/webhooks/123/abc,https://hooks.slack.com/services/T00/B00/xxx")
# From environment variable (reads SPECORA_HEALER_WEBHOOK_URL)
notifier = Notifier()
#specora-healer)export SPECORA_HEALER_WEBHOOK_URL="https://discord.com/api/webhooks/123456789/abcdefghijk..."
#specora-healer)export SPECORA_HEALER_WEBHOOK_URL="https://hooks.slack.com/services/T00000000/B00000000/xxxxxxxxxxxxxxxxxxxxxxxx"
... menu > Connectors (or Workflows)export SPECORA_HEALER_WEBHOOK_URL="https://outlook.office.com/webhook/..."
Any URL that is not Discord, Slack, or Teams receives the raw JSON payload. Use this for:
export SPECORA_HEALER_WEBHOOK_URL="https://my-api.example.com/healer-events"
All three platforms receive the same Markdown-formatted message:
[icon] **Specora Healer -- [EVENT]**
**Contract:** `entity/helpdesk/ticket`
**Priority:** critical | **Tier:** 3
[message content, up to 800 characters]
[link] [View ticket](http://localhost:8083/healer/tickets/abc.../view)
The [View ticket] link points to the HTML approval page where you can review the proposal and click Approve or Reject.
Generic webhooks receive the full payload:
{
"timestamp": "2025-01-15T10:30:00+00:00",
"event": "proposed",
"ticket_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"contract_fqn": "entity/helpdesk/ticket",
"status": "proposed",
"tier": 3,
"priority": "critical",
"message": "Add 'resolution' field (type: text) to fix KeyError on PATCH."
}
| Event | Icon | Description |
|---|---|---|
queued |
(inbox) | Error received and queued for processing |
proposed |
(lightbulb) | Fix proposed, awaiting approval (Tier 2-3 only) |
approved |
(thumbs up) | Fix approved by human |
applied |
(checkmark) | Fix applied to contract and code regenerated |
failed |
(cross) | Processing failed (unfixable, generator bug, data issue) |
rejected |
(no entry) | Human rejected the proposed fix |
(lightbulb) **Specora Healer -- PROPOSED**
**Contract:** `entity/helpdesk/ticket`
**Priority:** high | **Tier:** 2
Add required 'severity' field with enum [critical, high, medium, low]
to match workflow guard requirement.
(link) [View ticket](http://localhost:8083/healer/tickets/a1b2.../view)
(checkmark) **Specora Healer -- APPLIED**
**Contract:** `entity/helpdesk/ticket`
**Priority:** high | **Tier:** 1
Deterministic normalization: spec.fields.myField -> spec.fields.my_field;
spec.fields.assignedTo.references.graph_edge: 'assigned_to' -> 'ASSIGNED_TO'
(link) [View ticket](http://localhost:8083/healer/tickets/b2c3.../view)
When code is regenerated after a fix, a second notification fires:
(checkmark) **Specora Healer -- APPLIED**
**Contract:** `entity/helpdesk/ticket`
**Priority:** critical | **Tier:** 3
(cycle) Auto-regenerated: 42 files regenerated
(link) [View ticket](http://localhost:8083/healer/tickets/c3d4.../view)
(cross) **Specora Healer -- FAILED**
**Contract:** `entity/helpdesk/ticket`
**Priority:** critical | **Tier:** 3
(gear) Generator bug (not a contract issue): column 'severity' does not exist
(link) [View ticket](http://localhost:8083/healer/tickets/d4e5.../view)
(cross) **Specora Healer -- FAILED**
**Contract:** `entity/helpdesk/ticket`
**Priority:** high | **Tier:** 3
(disk) Data issue (not a contract issue): duplicate key value violates unique constraint
(link) [View ticket](http://localhost:8083/healer/tickets/e5f6.../view)
(no entry) **Specora Healer -- REJECTED**
**Contract:** `entity/helpdesk/ticket`
**Priority:** high | **Tier:** 2
Wrong approach, need to add a workflow guard instead
(link) [View ticket](http://localhost:8083/healer/tickets/f6a7.../view)
The [View ticket] link in every notification leads to:
http://localhost:{SPECORA_HEALER_PORT}/healer/tickets/{ticket_id}/view
Default port is 8083 (configurable via SPECORA_HEALER_PORT environment variable).
The page shows:
proposed): clicking Approve applies the fix and regenerates code; clicking Reject marks the ticket as rejectedAfter clicking Approve or Reject, the page redirects back to itself showing the updated status.
Every notification is always sent to all three channels:
| Channel | Always Active | Description |
|---|---|---|
| Console | Yes | Rich-formatted colored output via rich.console |
| File | Yes | JSONL log at .forge/healer/notifications.jsonl |
| Webhooks | Only if configured | HTTP POST to each URL in SPECORA_HEALER_WEBHOOK_URL |
[green][healer/applied][/green] entity/helpdesk/ticket: Deterministic normalization: spec.fields...
[cyan][healer/proposed][/cyan] entity/helpdesk/ticket: Add 'resolution' field (type: text)...
[red][healer/failed][/red] entity/helpdesk/ticket: Generator bug (not a contract issue)...
Each line in .forge/healer/notifications.jsonl is a JSON object:
{"timestamp": "2025-01-15T10:30:00+00:00", "event": "applied", "ticket_id": "a1b2...", "contract_fqn": "entity/helpdesk/ticket", "status": "applied", "tier": 1, "priority": "high", "message": "Deterministic normalization: ..."}
from healer.notifier import Notifier
from healer.models import HealerTicket, TicketSource
# Create notifier (reads SPECORA_HEALER_WEBHOOK_URL from env)
notifier = Notifier()
# Create notifier with explicit webhook URL
notifier = Notifier(webhook_url="https://discord.com/api/webhooks/123/abc")
# Create notifier with custom log path
notifier = Notifier(log_path=Path(".forge/healer/notifications.jsonl"))
# Send a notification
ticket = HealerTicket(source=TicketSource.RUNTIME, raw_error="KeyError: 'resolution'")
notifier.notify(ticket, event="proposed", message="Add 'resolution' field to fix KeyError")
The notify() method is called automatically by the pipeline. You only need to call it directly for custom notifications.