Build a Webhook-Triggered Workflow
In Lesson 1, you clicked "Run" to start a workflow. That's fine for testing, but real workflows start from external events — Stripe fires a payment webhook, GitHub pushes a commit event, a form submits data. The workflow needs to wake up when those events arrive.
Webhook triggers solve this. The workflow gets a URL. POST to that URL, and the workflow runs with whatever data you sent. The API returns instantly (same pattern as Hello Workflow), and the workflow processes the event durably in the background.
The workflow has a URL. It waits for a POST. When the POST arrives, execution continues exactly where it left off. No polling, no state management — the workflow literally suspends and resumes. This is the same pattern as createWebhook() in the SDK.
Outcome
You'll build a webhook-triggered workflow, POST JSON to it with curl, and watch the workflow process that data through multiple steps.
Fast Track
- Create a new workflow with Webhook trigger
- Add an HTTP Request action that echoes the data
- Copy the webhook URL and POST to it with curl
Hands-on Exercise
1. Create a Webhook-Triggered Workflow
- Click New Workflow (or modify your existing one)
- Click the Trigger node to open the properties panel
- Change Trigger Type from "Manual" to Webhook
- Change the Label from "Manual Trigger" to Webhook Trigger
- You'll see a Webhook URL appear — copy it (or wait until after you save)
The URL looks like:
http://localhost:3000/api/workflows/abc123/webhook
2. Add an HTTP Request Action
We'll use JSONPlaceholder to echo back our data. This proves the webhook payload flows through the workflow.
- Click the + button after the trigger
- Select HTTP Request from the action grid
- Configure it:
- URL:
https://jsonplaceholder.typicode.com/posts - HTTP Method:
POST - Body:
{ "title": "{{@trigger-1:Webhook Trigger.title}}", "body": "{{@trigger-1:Webhook Trigger.body}}", "userId": 1 }
- URL:
The syntax {{@trigger-1:Webhook Trigger.title}} has three parts:
trigger-1— the node ID (visible in the properties panel)Webhook Trigger— the node label (must match exactly).title— the property from the trigger payload
Both the node ID and label must match. If you skipped step 4 (renaming the label), your template won't resolve. Go back and change the label to "Webhook Trigger" to match the template.
3. Save the Workflow
Click Save in the toolbar. The webhook URL is now active.
The URL includes the workflow ID, which is assigned when you save. No save = no URL.
4. Set Up Mock Data for Testing
Before we use curl, let's set up mock data so you can also test from the UI:
- Click the Trigger node
- Find Mock Request (Optional)
- Enter:
{ "title": "Test from UI", "body": "This is mock data for testing" }
Now you can click Run in the toolbar to test with mock data, or POST real data via curl.
Try It
Open a terminal and POST to your webhook URL:
curl -X POST http://localhost:3000/api/workflows/YOUR_WORKFLOW_ID/webhook \
-H "Content-Type: application/json" \
-d '{"title": "Hello from curl", "body": "Webhook triggered!"}'You should see:
{"executionId":"exec_xyz","status":"running"}The response came back instantly — the workflow is running in the background.
Check your terminal (where pnpm dev is running):
[Webhook] Starting execution: exec_xyz
[Webhook] Calling executeWorkflow with: { nodeCount: 2, edgeCount: 1, ... }
[Workflow Executor] Starting workflow execution
[Workflow Executor] Executing trigger node
[Workflow Executor] Executing action node: HTTP Request
[Workflow Executor] Workflow execution completed: { success: true, ... }
Check the Runs panel in the UI — you'll see the execution with:
- Trigger node showing the data you POSTed
- HTTP Request node showing the response from JSONPlaceholder
Predict the Output
If you POST { 'title': 'My Title' } (without a 'body' field), what will the HTTP Request send to JSONPlaceholder? What will the template {{@trigger-1:Webhook Trigger.body}} resolve to?
Solution
The webhook flow works like this:
- External system POSTs to your workflow's webhook URL with JSON data
- API records the execution and returns immediately with
{ executionId, status: "running" } - Workflow runs in background with the POST body available as trigger output
- Template variables like
{{@trigger-1:Webhook Trigger.fieldName}}pull data from the payload
When to use webhooks vs manual triggers:
| Use Case | Trigger Type |
|---|---|
| Third-party events (Stripe, GitHub, Slack) | Webhook |
| Scheduled jobs (via external cron) | Webhook |
| User-initiated actions in your app | Webhook (POST from your frontend/backend) |
| Testing and development | Manual (with mock data) |
| One-off administrative tasks | Manual |
Webhooks are the production pattern. Manual triggers exist for testing workflows before wiring them to real event sources.
What's Happening
The webhook endpoint does three things:
- Validates — checks the workflow exists and is configured for webhooks
- Records — creates an execution record with the incoming payload
- Starts — calls
start(executeWorkflow, [...])and returns immediately
// Parse the incoming POST body
const body = await request.json().catch(() => ({}));
// Create execution record with the webhook payload
const [execution] = await db
.insert(workflowExecutions)
.values({
workflowId,
userId: workflow.userId,
status: "running",
input: body, // ← Your curl data lands here
})
.returning();
// Start workflow in background (don't await)
executeWorkflowBackground(
execution.id,
workflowId,
workflow.nodes,
workflow.edges,
body // ← And gets passed to the workflow
);
// Return immediately
return NextResponse.json({
executionId: execution.id,
status: "running",
});The workflow receives the POST body as triggerInput. Template variables like {{@trigger-1:Webhook Trigger.title}} pull from this data.
You POST { 'user': 'alice', 'action': 'signup' } to the webhook. In your HTTP Request body, how do you access the 'action' field?
This starter skips signature verification for simplicity. In production, always verify webhook signatures to prevent replay attacks and spoofed requests. Stripe, GitHub, and most providers include a signature header — see Stripe's webhook signature docs for a reference implementation.
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: Request) {
// Read raw body for signature verification
const body = await request.text();
const sig = request.headers.get('stripe-signature')!;
try {
// Verify the webhook came from Stripe
const event = stripe.webhooks.constructEvent(
body,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
);
// Safe to process - signature verified
// Pass event.data.object to your workflow...
} catch (err) {
// Invalid signature - reject the request
return new Response('Invalid signature', { status: 400 });
}
}Debugging: "This workflow is not configured for webhook triggers"
If you see this error when POSTing:
{"error":"This workflow is not configured for webhook triggers"}The trigger type is still set to "Manual". Open the workflow, click the Trigger node, and change the Trigger Type dropdown to "Webhook". Save again.
Debugging: Empty Trigger Data
If your HTTP Request step shows empty strings where you expected data:
- Check your curl command — is the JSON valid? Missing quotes?
- Check the template syntax — it's
{{@nodeId:Label.fieldName}}, not{{trigger.fieldName}} - Check the node ID — click your trigger node and verify the ID matches (e.g.,
trigger-1) - Check field names —
.titlewon't findTitle(case matters)
Commit
git add -A
git commit -m "feat: add webhook-triggered workflow with HTTP request"Done
- Changed trigger type to Webhook
- Copied the webhook URL
- Added HTTP Request action with template variables
- POSTed JSON with curl
- Saw instant response, workflow completed in background
- Verified trigger data flowed to the HTTP Request step
The Workflow SDK provides powerful primitives for handling external events. See Hooks & Webhooks for patterns like waiting for multiple events, custom tokens, and manual response handling.
What's Next
Webhook triggers let external systems start your workflows. But the built-in actions (Log, HTTP Request, Condition) are limited. What if you want to send emails, post to Slack, create Linear tickets, or call your internal APIs?
That's where plugins come in. Lesson 3 teaches you the plugin folder pattern — how to extend the workflow builder with your own actions.
Was this helpful?