Steroid Kit logo

Steroid_Kit

Steroid Flows›Webhook & REST API

Webhook & REST API Pattern

Any flow with a Webhook trigger is immediately a REST endpoint. The caller sends a POST request, the flow base Live URL starts asynchronous execution. Append /sync to receive a Return Response body or /sync-stream for SSE lifecycle events.

Core Idea

The webhook pattern turns a visual flow into a function: input arrives as JSON, steps transform it, output leaves through the selected response mode. Authentication is optional by default; published schema metadata is not blanket runtime input/output validation.

The REST API pattern

POST /api/v1/webhooks/{flow-id}/sync
Body: { ...input }

  ↓  Webhook Trigger  — receives body, exposes as {{trigger.body.*}}
  ↓  Step 1           — MCP block or LLM node
  ↓  Step 2           — MCP block or LLM node
  ↓  ...
  ↓  Return Response  — sets status code + response body

HTTP {status}
Body: { ...output }

Webhook Trigger

The Webhook trigger is the entry point for REST-style invocations. When you add one to a flow, Steroid generates a Live URL for that flow. Copy the generated URL; the base path is asynchronous, not the synchronous response endpoint.

  • Accepts any JSON body — no schema enforcement at the trigger level.
  • Authentication choices are None (default), Basic Auth and Header Auth. There is no native HMAC option in this trigger.
  • Body fields are available as {{trigger.body.field_name}} in all downstream steps.
  • Query string parameters are available as {{trigger.queryParams.param}}.

Endpoint format

<Live URL>                 # asynchronous base endpoint
<Live URL>/sync            # synchronous HTTP response body
<Live URL>/sync-stream     # text/event-stream, not a single JSON body

curl — minimal call

curl --fail-with-body -X POST "<LiveUrl in the Webhook Trigger node>/sync" \
  -H "Content-Type: application/json" \
  -d '{"repo": "acme/backend", "since": "2025-01-01"}'

Accessing trigger data in steps

# In any step's input fields:
{{trigger.body.repo}}          → "acme/backend"
{{trigger.body.since}}         → "2025-01-01"
{{trigger.queryParams.format}} → value of ?format= in the URL
{{trigger.headers.x-user-id}}  → value of the X-User-Id header

Return Response Block

The Return Response block ends synchronous execution and sends the HTTP response back to the caller. Every flow that is called as a REST API should end with one.

  • Set a fixed or dynamic HTTP status code (200, 201, 400, etc.).
  • Build the response body from any combination of previous step outputs.
  • Set arbitrary response headers (Content-Type, Cache-Control, etc.).
  • Use an explicit Return Response when callers need a body; test the actual status/body instead of assuming an omitted response is successful.

Return Response — example config

{
  "status": 200,
  "headers": {
    "Content-Type": "application/json"
  },
  "body": {
    "llm_result": "{{step_1}}",
    "github_result": "{{step_2}}",
    "slack_result": "{{step_3}}"
  }
}

Schematic HTTP response — actual envelopes vary

HTTP/1.1 200 OK
Content-Type: application/json

{
  "llm_result": { "success": true, "output": {}, "executionTime": 0 },
  "github_result": {},
  "slack_result": {}
}

Calling Your Flow

The webhook URL works from any HTTP client. Below are patterns for the four most common callers.

Python Script

Python — call a flow

import httpx

response = httpx.post(
    "<LiveUrl in the Webhook Trigger node>/sync",
    json={
        "owner":    "acme",
        "repo":     "backend",
        "channel_id": "C0123456789",
        "pr_title": "Optimise worker pool",
        "diff":     open("pr.diff").read(),
    },
    timeout=660,
)

response.raise_for_status()
result = response.json()
print(result["github_result"])

SSE stream

Parse data events, not response.json()

import json
import httpx

with httpx.stream(
    "POST",
    "<LiveUrl in the Webhook Trigger node>/sync-stream",
    json={"prompt": "Hello"},
    timeout=660,
) as response:
    response.raise_for_status()
    data_lines = []
    completed = False
    for line in response.iter_lines():
        if line.startswith("data:"):
            data_lines.append(line[5:].lstrip())
        elif not line and data_lines:
            event = json.loads("\n".join(data_lines))
            data_lines.clear()
            print(event)
            if event.get("type") == "FLOW_COMPLETED":
                print(event["response"]["body"])
                completed = True
    if not completed:
        raise RuntimeError("Stream ended without a final response")

Use a payload matching the actual flow; the SSE example targets the prompt-only starter. Inspect error/lifecycle events as well as the final body. Disconnecting the stream emits a cancellation signal, but does not guarantee immediate cancellation or undo of every external side effect.

GitHub Actions

.github/workflows/review.yml

- name: Trigger Steroid review flow
  env:
    STEROID_WEBHOOK_URL: ${{ secrets.STEROID_WEBHOOK_URL }}
  run: |
    curl --fail-with-body -X POST "$STEROID_WEBHOOK_URL/sync" \
      -H "Content-Type: application/json" \
      -d '{
        "owner": "acme",
        "repo": "backend",
        "channel_id": "C0123456789",
        "pr_title": "Review request",
        "diff": "Example test input"
      }'

Another Flow

HTTP block config — call a sub-flow

{
  "method": "POST",
  "url":    "<LiveUrl in the Webhook Trigger node>/sync",
  "body": {
    "data": "{{step_1}}"
  }
}

Structured Output

Choose Invoke Structured LLM with jsonSchema when generation needs a typed contract. Inspect its success/output/executionTime wrapper and validate any final API contract explicitly. An AP publish schema or Return Response alone does not enforce every response shape.

TIP

Structured generation can fail parsing/validation or dispatch. Return Response passes its configured values through; do not treat this as a guarantee that every run produces schema-valid success.

LLM node structured output schema

{
  "type": "object",
  "properties": {
    "action":      { "type": "string", "enum": ["approve", "request_changes", "comment"] },
    "reason":      { "type": "string" },
    "suggestions": { "type": "array", "items": { "type": "string" } }
  },
  "required": ["action", "reason"]
}

Examples

These are design patterns, not ready-to-import exports. Select actual catalog tools, inspect output bindings, configure authentication and test real integrations before publishing.

Code Review Summariser

Accepts a PR diff, returns a structured review with risk level and suggested labels.

Flow chain

Catch Webhook → Invoke LLM → github-official issue_write → Return Response

Nightly Changelog Generator

Runs on a schedule, fetches merged PRs from GitHub, and posts a formatted digest to Slack.

Flow chain

Schedule → GitHub (list merged PRs) → LLM Node (format changelog) → Slack (post message)

Jira Ticket → Slack Standup

Pulls today's in-progress tickets and formats them as a standup message for the team channel.

Flow chain

Webhook → Jira (list tickets) → LLM Node (format standup) → Slack (post message) → Return Response

Natural Language DB Query

Accepts a plain-English question, converts it to SQL, runs it against Postgres, and returns results.

Flow chain

Webhook → LLM Node (text → SQL) → Postgres (run query) → LLM Node (format results) → Return Response

WARNING

Configure Basic/Header authentication when required and send the matching credentials from callers. Generated MCP HTTP calls use 120 seconds with up to two retries; Invoke LLM requests use 590 seconds, AP flows are configured for 600 and sync wait for 650. The example client timeout is a waiting budget, not a guarantee of completion. Avoid blindly retrying non-idempotent writes after a timeout.

On this page