
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 bodycurl — 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 headerReturn 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 ResponseNightly 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 ResponseNatural 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 ResponseWARNING
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.