Feat v0.3.6 demo workflows + interactive demo

Four example workflow packages proving the engine end-to-end:
- Bug Report Triage: public entry, branch routing, SLA timer
- Employee Onboarding: Starlark automated stages, signoff gate
- Content Approval: multi-party signoff, revision cycle loop
- Webhook Notifier: http.post, connections fallback, delivery logging

Demo surface at /s/workflow-demo with cards, stage diagrams, Starlark
viewer, API examples, active/published status, and copyable public links.

Engine fixes: started_by in automated context, sla_seconds in package
installer, parseSnapshotStages for wrapped/legacy formats.

Platform fixes: extension SDK boot in base.html (Preact globals + boot()),
admin teams paginated response extraction, workflow adoption endpoint
(POST /teams/:teamId/workflows/:id/adopt), team-admin copyable link.

Review pass remains on roadmap for remaining UI polish.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-28 15:48:47 +00:00
parent d68451fe8e
commit f9cd5bce0f
30 changed files with 1716 additions and 30 deletions

View File

@@ -0,0 +1,30 @@
# Webhook Notifier
Outbound webhook delivery with connection credentials and delivery logging.
## Features
- **Starlark `http.post`** — outbound HTTP with custom headers
- **`connections.get`** — fallback to stored webhook connection credentials
- **Delivery logging** — records URL, status code, event to `webhook_log` ext_data table
- **Result review** — delivery status shown in final review stage
## Stage Flow
```
[configure] → [fire-webhook] → [result]
team automated review
(http.post)
```
## Starlark Hook
- **`on_fire(ctx)`** — Posts JSON payload to webhook URL, falls back to `connections.get("webhook")`, logs delivery to `webhook_log`
## Installation
```bash
curl -X POST $BASE/api/v1/admin/packages/install \
-H "Authorization: Bearer $TOKEN" \
-F "file=@packages/webhook-notifier"
```

View File

@@ -0,0 +1,79 @@
{
"id": "webhook-notifier",
"title": "Webhook Notifier",
"type": "workflow",
"version": "0.1.0",
"tier": "starlark",
"icon": "🔔",
"author": "Switchboard Core",
"description": "Outbound webhook delivery with connection credential support and delivery logging.",
"permissions": ["api.http", "connections.read", "db.write"],
"db_tables": {
"webhook_log": {
"columns": {
"url": "text",
"status_code": "text",
"event": "text",
"instance_id": "text"
}
}
},
"workflow_definition": {
"name": "Webhook Notifier",
"slug": "webhook-notifier",
"entry_mode": "team_only",
"stages": [
{
"name": "configure",
"ordinal": 0,
"stage_mode": "form",
"audience": "team",
"stage_type": "simple",
"auto_transition": false,
"form_template": {
"fieldsets": [
{
"label": "Webhook Configuration",
"fields": [
{ "key": "webhook_url", "label": "Webhook URL", "type": "text", "required": true },
{ "key": "event_name", "label": "Event Name", "type": "text", "required": true },
{ "key": "payload", "label": "Custom Payload (JSON)", "type": "textarea" }
]
}
]
}
},
{
"name": "fire-webhook",
"ordinal": 1,
"stage_mode": "automated",
"audience": "system",
"stage_type": "automated",
"auto_transition": true,
"starlark_hook": "webhook-notifier:on_fire"
},
{
"name": "result",
"ordinal": 2,
"stage_mode": "review",
"audience": "team",
"stage_type": "simple",
"auto_transition": false,
"form_template": {
"fieldsets": [
{
"label": "Delivery Result",
"fields": [
{ "key": "delivery_status", "label": "Status", "type": "text" },
{ "key": "status_code", "label": "HTTP Status Code", "type": "text" },
{ "key": "response_body", "label": "Response", "type": "textarea" }
]
}
]
}
}
]
}
}

View File

@@ -0,0 +1,41 @@
def on_fire(ctx):
"""Fire outbound webhook with optional connection credentials."""
data = ctx["stage_data"]
url = data.get("webhook_url", "")
# Fallback to configured connection
if not url:
conn = connections.get("webhook")
if conn:
url = conn.get("url", "")
if not url:
return {"error": "No webhook URL configured"}
payload = json.encode({
"event": data.get("event_name", "workflow.notify"),
"instance_id": ctx["instance_id"],
"workflow_id": ctx["workflow_id"],
"data": data,
})
resp = http.post(url, body=payload, headers={
"Content-Type": "application/json",
"X-Switchboard-Event": data.get("event_name", "workflow.notify"),
})
# Log the delivery
db.insert("webhook_log", {
"url": url,
"status_code": str(resp["status"]),
"event": data.get("event_name", ""),
"instance_id": ctx["instance_id"],
})
status = "delivered" if int(resp["status"]) < 400 else "failed"
return {"advance": True, "data": {
"delivery_status": status,
"status_code": str(resp["status"]),
"response_body": resp["body"][:500],
}}