Overview
When a workflow reaches a human_approval step, Station can notify external systems via webhooks. This enables human-in-the-loop automation with:
Slack notifications with approve/reject buttons
ntfy.sh alerts for mobile push notifications
PagerDuty escalations for critical decisions
Custom dashboards showing pending approvals
┌─────────────────────────────────────────────────────────────────────────────┐
│ APPROVAL WEBHOOK FLOW │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────────┐
│ Workflow │
│ Running │
└──────┬───────┘
│
▼
┌───────────────────────┐
│ human_approval │
│ step │
└───────────┬───────────┘
│
▼
┌───────────────────────┐ ┌─────────────────────────────────────┐
│ Create Approval │ │ WEBHOOK NOTIFICATION │
│ Record in DB │ │ │
└───────────┬───────────┘ │ POST https://ntfy.sh/your-topic │
│ │ │
├────────────────▶│ { │
│ │ "event": "approval.requested", │
│ │ "approval_id": "appr-abc123", │
│ │ "message": "Approve deploy?", │
│ │ "approve_url": "http://...", │
│ │ "reject_url": "http://..." │
│ │ } │
│ └─────────────────────────────────────┘
│
▼
┌───────────────────────┐
│ WORKFLOW PAUSED │◄───────────────────────────────┐
│ (waiting_approval) │ │
└───────────┬───────────┘ │
│ │
│ Human reviews notification │
│ │
▼ │
┌──────────────┐ │
│ Decision │ │
└──────┬───────┘ │
│ │
┌───────┴───────┐ │
│ │ │
▼ ▼ │
┌────────┐ ┌──────────┐ │
│APPROVE │ │ REJECT │ │
└───┬────┘ └────┬─────┘ │
│ │ API Call │
│ ├───────────────────────────────────►│
│ │ POST /workflow-approvals/{id}/reject
│ API Call │ │
├──────────────┼───────────────────────────────────►│
│ │ POST /workflow-approvals/{id}/approve
▼ ▼
┌────────────────────────┐
│ Workflow Continues │
│ or Fails │
└────────────────────────┘
Configuration
Config File
Add to your config.yaml:
notifications :
# Webhook URL to POST when approval is requested
approval_webhook_url : "https://ntfy.sh/station-approvals"
# Timeout for webhook delivery (default: 10 seconds)
approval_webhook_timeout : 10
Environment Variables
export STN_APPROVAL_WEBHOOK_URL = "https://ntfy.sh/station-approvals"
export STN_APPROVAL_WEBHOOK_TIMEOUT = 10
Webhook Payload
When a workflow reaches a human_approval step, Station sends:
{
"event" : "approval.requested" ,
"approval_id" : "appr-run123-approve_deploy" ,
"workflow_id" : "deploy-production" ,
"workflow_name" : "Production Deployment" ,
"run_id" : "run-abc123" ,
"step_name" : "approve_deploy" ,
"message" : "Approve deployment of api-service v2.3.0 to production?" ,
"approvers" : [ "oncall-sre" , "platform-leads" ],
"timeout_seconds" : 1800 ,
"created_at" : "2025-01-04T17:30:00Z" ,
"approve_url" : "http://localhost:8587/workflow-approvals/appr-run123-approve_deploy/approve" ,
"reject_url" : "http://localhost:8587/workflow-approvals/appr-run123-approve_deploy/reject"
}
Payload Fields
Field Type Description eventstring Always "approval.requested" approval_idstring Unique ID for this approval request workflow_idstring ID of the workflow definition workflow_namestring Human-readable workflow name run_idstring ID of this workflow run instance step_namestring Name of the approval step messagestring Human-readable message (supports template variables) approversarray List of allowed approvers (optional) timeout_secondsint Seconds until approval expires created_atstring ISO 8601 timestamp approve_urlstring URL to POST for approval reject_urlstring URL to POST for rejection
Integration Examples
ntfy.sh
ntfy is a simple HTTP-based pub-sub notification service - perfect for mobile push notifications.
Subscribe to your topic
Open the app and subscribe to station-approvals (or your chosen topic name)
Configure Station
notifications :
approval_webhook_url : https://ntfy.sh/station-approvals
approval_webhook_timeout : 10
Test the webhook
curl -d "Test notification from Station" https://ntfy.sh/station-approvals
For rich notifications with titles, priorities, and action buttons, use a webhook proxy that transforms Station’s JSON payload into ntfy’s format. See the advanced examples below.
Slack
Configure a Slack Incoming Webhook:
notifications :
approval_webhook_url : "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXX"
For interactive approve/reject buttons, you’ll need a small webhook proxy to transform the payload into Slack Block Kit format:
{
"blocks" : [
{
"type" : "section" ,
"text" : {
"type" : "mrkdwn" ,
"text" : "*Approval Required* \n\n Approve deployment of `api-service v2.3.0` to production?"
}
},
{
"type" : "actions" ,
"elements" : [
{
"type" : "button" ,
"text" : { "type" : "plain_text" , "text" : "Approve" },
"style" : "primary" ,
"url" : "http://station:8587/workflow-approvals/appr-abc123/approve"
},
{
"type" : "button" ,
"text" : { "type" : "plain_text" , "text" : "Reject" },
"style" : "danger" ,
"url" : "http://station:8587/workflow-approvals/appr-abc123/reject"
}
]
}
]
}
Send approval requests as PagerDuty incidents:
notifications :
approval_webhook_url : "https://events.pagerduty.com/v2/enqueue"
You’ll need a webhook proxy to transform to PagerDuty Events API v2 format.
Custom Webhook Proxy
Build a simple proxy to transform Station’s payload for any system:
from flask import Flask, request
import requests
app = Flask( __name__ )
NTFY_TOPIC = "https://ntfy.sh/station-approvals"
@app.route ( '/webhook' , methods = [ 'POST' ])
def handle_approval ():
data = request.json
# Build ntfy message with actions
message = f " { data[ 'message' ] } \n\n Workflow: { data[ 'workflow_name' ] } "
headers = {
"Title" : "Approval Required" ,
"Priority" : "high" ,
"Tags" : "warning" ,
"Actions" : f "http, Approve, { data[ 'approve_url' ] } ; http, Reject, { data[ 'reject_url' ] } "
}
requests.post( NTFY_TOPIC , data = message, headers = headers)
return { "status" : "ok" }
if __name__ == '__main__' :
app.run( port = 5000 )
Then configure Station to use your proxy:
notifications :
approval_webhook_url : http://localhost:5000/webhook
Approval API
Once notified, approvers can use the URLs in the payload or call the API directly.
Approve a Request
curl -X POST http://localhost:8587/workflow-approvals/{approval_id}/approve \
-H "Content-Type: application/json" \
-d '{"comment": "Reviewed and approved"}'
Response:
{
"approval_id" : "appr-abc123" ,
"status" : "approved" ,
"decided_by" : "api-user" ,
"decided_at" : "2025-01-04T17:35:00Z"
}
Reject a Request
curl -X POST http://localhost:8587/workflow-approvals/{approval_id}/reject \
-H "Content-Type: application/json" \
-d '{"reason": "Missing security review"}'
CLI Commands
# List pending approvals
stn workflow approvals list --status pending
# Approve
stn workflow approvals approve appr-abc123 --comment "LGTM"
# Reject
stn workflow approvals reject appr-abc123 --reason "Needs review"
# Get details
stn workflow approvals get appr-abc123
Audit Logging
All webhook deliveries and approval decisions are logged for compliance and debugging.
Webhook Delivery Logs
Every webhook attempt is recorded with:
Request payload sent
Response status code
Response body (truncated)
Duration in milliseconds
Error message (if failed)
View Audit Trail
curl http://localhost:8587/workflow-approvals/{approval_id}/audit
Response:
{
"logs" : [
{
"event_type" : "webhook.attempt" ,
"webhook_url" : "https://ntfy.sh/station-approvals" ,
"created_at" : "2025-01-04T17:30:00.100Z"
},
{
"event_type" : "webhook.success" ,
"response_status" : 200 ,
"duration_ms" : 150 ,
"created_at" : "2025-01-04T17:30:00.250Z"
},
{
"event_type" : "approval.approved" ,
"metadata" : {
"decided_by" : "alice@example.com" ,
"comment" : "Looks good"
},
"created_at" : "2025-01-04T17:35:00Z"
}
]
}
Audit Event Types
Event Type Description webhook.attemptWebhook delivery started webhook.successWebhook delivered successfully (2xx) webhook.failureWebhook delivery failed approval.approvedApproval was granted approval.rejectedApproval was denied approval.timeoutApproval expired without decision
Retry Behavior
Station automatically retries failed webhook deliveries:
Max retries: 3 attempts
Backoff: Exponential (1s, 4s, 9s)
Timeout: Configurable per-request (default 10s)
All retry attempts are logged in the audit trail.
Troubleshooting
Verify webhook URL is correct:
curl -X POST "YOUR_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d '{"test": true}'
Check Station logs:
STN_LOG_LEVEL = debug stn serve 2>&1 | grep -i webhook
Check audit logs:
curl http://localhost:8587/workflow-approvals/{id}/audit
Check the audit log for the response body: SELECT event_type, response_status, error_message
FROM notification_logs
WHERE approval_id = 'appr-xxx'
ORDER BY created_at DESC ;
Verify Station is accessible from where approvals happen
Check approval hasn’t expired: stn workflow approvals get appr-xxx
Check approval wasn’t already decided
Workflow stuck after approval
Check workflow run status: stn workflow runs get run-xyz --steps
Check NATS is running: stn status
Check logs for errors
Security Considerations
Always use HTTPS for webhook URLs in production to protect approval payloads.
Recommendations
Use HTTPS - Encrypt webhook traffic
Protect approval API - Run Station on internal network or use authentication
Audit retention - Configure log retention for compliance
Validate approvers - Use the approvers field to restrict who can approve
Comparison: Notifications vs Approval Webhooks
Feature Notify Tool Approval Webhooks Trigger Agent calls notify() Workflow hits human_approval step Purpose Alert users about events Block workflow until human decision Payload Customizable message Structured approval request Response Fire-and-forget Approve/reject via API Config notify: sectionnotifications: section
Next Steps