Webhook to Slack Flow

A complete example flow that receives webhooks and sends notifications to Slack, built entirely from shipped components.

Overview

This flow demonstrates:

  • HTTP webhook reception (http_server, http-module)
  • Conditional routing (router, common-module)
  • External API integration (http_request, http-module)
  • Data mapping with edge expressions — no glue components
  • Error handling with the retry component

Flow Architecture

                                       +---------------+
                                  +--->| out_critical  |---+
                                  |    +---------------+   |
+--------+   +-----------+   +----+----------+             v
| signal |-->| http_server|-->|    router     |    +--------------+   +-------------+
| (Start)|   | request -->|   | (by severity) |--->| http_request |-->| http_server |
+--------+   +-----------+   +----+----------+    | (Slack API)  |   |  response   |
                                  |    +---------+ +------+-------+   +-------------+
                                  +--->| default |        |
                                       | (drop / |        | error
                                       |  debug) |        v
                                       +---------+   +---------+
                                                     |  retry  |
                                                     +---------+

All components are real: signal, router, retry, debug from common-module; http_server, http_request from http-module.

Node Configurations

1. Webhook Server

Component: http-module/http_server

The server does not run until its start port receives a message — wire a signal node into start and click Send once. The public URL then appears on the server's _control port (listenAddr); never guess the address.

_settings port configuration:

{
  "enableStatusPort": false,
  "enableStopPort": true
}

Edge signal.out -> webhook-server.start configuration:

{
  "autoHostName": true,
  "readTimeout": 60,
  "writeTimeout": 10
}

Each incoming HTTP request emits on the request port as {context, requestURI, requestParams, host, method, realIP, headers, body, scheme}. The body is a raw string.

2. Severity Router

Component: common-module/router

_settings port configuration:

{
  "routes": ["CRITICAL", "WARNING"],
  "enableDefaultPort": true
}

This produces output ports out_critical, out_warning, and default.

Edge webhook-server.request -> severity-router.input configuration:

Senders call POST /webhook/alerts?severity=critical&source=db-monitor with the alert text as the body. The conditions are boolean-returning expressions evaluated per message:

{
  "context": {
    "severity": "{{$.requestParams.severity[0]}}",
    "source": "{{$.requestParams.source[0]}}",
    "message": "{{$.body}}",
    "receivedAt": "{{RFC3339(now())}}"
  },
  "conditions": [
    { "route": "CRITICAL", "condition": "{{$.requestParams.severity[0] == 'critical' || $.requestParams.severity[0] == 'error'}}" },
    { "route": "WARNING",  "condition": "{{$.requestParams.severity[0] == 'warning'}}" }
  ]
}

The router forwards the payload under a context key: each out_<route> port emits { "context": {...} }, so downstream edges read $.context.severity — not $.severity.

3. Slack API Client

Component: http-module/http_request

_settings port configuration:

{
  "enableErrorPort": true
}

Edge severity-router.out_critical -> slack-client.request configuration:

The Slack token comes from a secret placeholder — never a literal:

{
  "context": "{{$.context}}",
  "method": "POST",
  "timeout": 10,
  "url": "https://slack.com/api/chat.postMessage",
  "contentType": "application/json",
  "headers": [
    { "key": "Authorization", "value": "Bearer [[secret:slack/botToken]]" }
  ],
  "body": "{\"channel\": \"#alerts-critical\", \"text\": \"🚨 {{upper($.context.severity)}} from {{$.context.source}}: {{$.context.message}}\"}"
}

Edge severity-router.out_warning -> slack-client.request is identical except channel and prefix:

{
  "context": "{{$.context}}",
  "method": "POST",
  "timeout": 10,
  "url": "https://slack.com/api/chat.postMessage",
  "contentType": "application/json",
  "headers": [
    { "key": "Authorization", "value": "Bearer [[secret:slack/botToken]]" }
  ],
  "body": "{\"channel\": \"#alerts-warning\", \"text\": \"⚠️ {{$.context.source}}: {{$.context.message}}\"}"
}

severity-router.default — leave it unwired to silently drop unknown severities, or wire it to a debug node (common-module) to inspect them on the dashboard.

4. Response to the Webhook Caller

Edge slack-client.response -> webhook-server.response configuration:

http_server blocks each HTTP request until data arrives back on its response port, so the flow answers the webhook caller by completing the loop:

{
  "statusCode": 200,
  "contentType": "application/json",
  "body": "{\"status\": \"accepted\", \"severity\": \"{{$.context.severity}}\"}"
}

5. Retry on Slack Failures

Component: common-module/retry

http_request marks network failures, 429 and 5xx as retryable (retryable: true on its error port — the canonical {context, error, retryable} shape). Wire the loop explicitly:

  • slack-client.error -> retry-slack.request (map context, retryable, error straight through)
  • retry-slack.retry -> slack-client.request (re-map the request fields from $.context)
  • retry-slack.failed -> debug (dead-letter for alerting)

_settings port configuration:

{
  "maxAttempts": 5,
  "initialDelayMs": 500,
  "maxDelayMs": 30000,
  "backoff": "exponential",
  "jitter": true
}

Non-retryable errors (4xx — bad channel, revoked token) go straight to failed without sleeping.

Complete Flow Definition

The editor and the MCP tools (build_flow) write these resources for you; shown here decoded for readability (port configuration is JSON stored as bytes). Edges are declared on the node that owns the source port, with to as "node:port"; edge data mapping lives in the target node's spec.ports entry with from set:

apiVersion: operator.tinysystems.io/v1alpha1
kind: TinyNode
metadata:
  name: webhook-server
  namespace: tinysystems
spec:
  module: http-module
  component: http_server
  ports:
    - port: _settings
      configuration: >
        {"enableStatusPort": false, "enableStopPort": true}
  edges:
    - id: edge-request-router
      port: request
      to: "severity-router:input"
      flowID: flow-webhook-alerts
---
apiVersion: operator.tinysystems.io/v1alpha1
kind: TinyNode
metadata:
  name: severity-router
  namespace: tinysystems
spec:
  module: common-module
  component: router
  ports:
    - port: _settings
      configuration: >
        {"routes": ["CRITICAL", "WARNING"], "enableDefaultPort": true}
    - port: input
      from: "webhook-server:request"
      configuration: >
        {"context": {"severity": "{{$.requestParams.severity[0]}}",
                     "source": "{{$.requestParams.source[0]}}",
                     "message": "{{$.body}}"},
         "conditions": [
           {"route": "CRITICAL", "condition": "{{$.requestParams.severity[0] == 'critical'}}"},
           {"route": "WARNING", "condition": "{{$.requestParams.severity[0] == 'warning'}}"}]}
  edges:
    - id: edge-critical-slack
      port: out_critical
      to: "slack-client:request"
      flowID: flow-webhook-alerts
    - id: edge-warning-slack
      port: out_warning
      to: "slack-client:request"
      flowID: flow-webhook-alerts
# ... slack-client and retry-slack nodes follow the same shape

Note there is no data field on an edge — an edge is {id, port, to, flowID} plus an optional retryPolicy; all mapping lives in port configurations.

Testing the Flow

Sample Webhook Payloads

Read the real URL from the server's _control port first.

Critical Alert:

curl -X POST "https://<listenAddr>/webhook/alerts?severity=critical&source=db-monitor" \
  -H "Content-Type: text/plain" \
  -d 'Unable to connect to primary database. Failover initiated.'

Warning Alert:

curl -X POST "https://<listenAddr>/webhook/alerts?severity=warning&source=metrics-collector" \
  -d 'Memory usage at 85%. Consider scaling.'

Info Alert (falls through to default — dropped or debugged):

curl -X POST "https://<listenAddr>/webhook/alerts?severity=info&source=ci-pipeline" \
  -d 'Version 2.3.1 deployed successfully'

Expected Slack Output

Critical Alert (#alerts-critical):

🚨 CRITICAL from db-monitor: Unable to connect to primary database. Failover initiated.

Warning Alert (#alerts-warning):

⚠️ metrics-collector: Memory usage at 85%. Consider scaling.

Key Patterns Used

1. Mapping on Edges, Not Glue Nodes

Normalization happens in edge expressions (JSONPath over the source port's output, ternaries, &&/||, functions like upper(), now(), RFC3339()). The transform component exists for same-node loops (e.g. request→response on one http_server), not as inter-node glue.

2. Conditions Computed Per Message

The router holds no comparison logic — booleans are computed on the edge, and the first true condition wins:

{ "route": "CRITICAL", "condition": "{{$.requestParams.severity[0] == 'critical'}}" }

3. Blocking Request/Response Loop

http_server.request → ... → http_server.response completes synchronously; the webhook caller gets the real outcome, not a blind 200.

4. Explicit, Bounded Retry

Retry is a visible loop in the graph (error → retry → request), honoring the retryable flag — nothing retries implicitly.

Extending the Flow

Add Deduplication

Prevent duplicate alerts with database-module/redis_dedup between the router and the Slack client — it routes first-seen IDs to out_new and repeats to out_seen:

{
  "context": "{{$.context}}",
  "url": "redis://redis.internal:6379",
  "keyPrefix": "alerts",
  "id": "{{$.context.source}}-{{$.context.severity}}",
  "ttlSeconds": 300
}

Add Acknowledgment Tracking

Store alerts with database-module/postgres_exec:

{
  "dsn": "postgres://app:[[secret:db/password]]@db.internal:5432/alerts",
  "sql": "INSERT INTO alerts (severity, source, message, created_at) VALUES ($1, $2, $3, $4)",
  "params": ["{{$.context.severity}}", "{{$.context.source}}", "{{$.context.message}}", "{{RFC3339(now())}}"]
}

Guard Costs

Put common-module/budget_guard in front of paid downstream calls to cap spend per window.

Monitoring

Track flow metrics with common-module/flow_telemetry, or via traces:

  • Webhook requests/minute: Monitor incoming traffic
  • Slack API latency: Track external API performance
  • Retry exhaustion (failed port) rate: Alert on delivery failures
  • Route distribution: See which severity levels are most common