API Triggers
Trigger workflows programmatically via the REST API
API Triggers
API triggers allow you to start workflows programmatically via the REST API. This is useful for on-demand processing, testing, or integrating with systems that don't support webhooks.
Configuration
API triggers have the simplest configuration:
{
"trigger": {
"type": "api"
}
}| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Must be "api" |
That's it! The workflow can now be triggered via the REST API.
Authoring in Studio
In the Studio canvas, add the API Trigger block from the Triggers section
of the block palette. It is a read-only block that shows the run endpoint for the
current workflow and confirms the request body is exposed downstream as
{{input}}. No configuration is required -- the trigger is ready as soon as the
workflow is active.
Running a Workflow
Basic Request
curl -X POST http://localhost:3000/api/v1/workflows/{WORKFLOW_ID}/run \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": {
"customerId": "cus_xxx",
"amount": 9900
}
}'const response = await fetch(
`http://localhost:3000/api/v1/workflows/${workflowId}/run`,
{
method: 'POST',
headers: {
'x-api-key': process.env.API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
input: {
customerId: 'cus_xxx',
amount: 9900
}
})
}
);
const { data } = await response.json();
console.log('Run ID:', data.runId);import requests
response = requests.post(
f'http://localhost:3000/api/v1/workflows/{workflow_id}/run',
headers={
'x-api-key': api_key,
'Content-Type': 'application/json'
},
json={
'input': {
'customerId': 'cus_xxx',
'amount': 9900
}
}
)
data = response.json()['data']
print(f"Run ID: {data['runId']}")Request Body
{
"input": {
// Your workflow input data
},
"connectionId": "conn_xxx",
"idempotencyKey": "unique-key-123"
}| Field | Type | Required | Description |
|---|---|---|---|
input | object | No | Input data passed to the workflow |
connectionId | string | No | Specific connection to use |
idempotencyKey | string | No | Prevent duplicate runs |
Response
{
"success": true,
"data": {
"runId": "run_xyz789...",
"status": "pending",
"workflowId": "wf_abc123...",
"workflowVersion": 3
}
}| Field | Description |
|---|---|
runId | Unique identifier for this run |
status | Initial status (pending) |
workflowId | The workflow being executed |
workflowVersion | Version of the workflow used |
Input Data
The input object is available throughout your workflow as {{input}}:
Request:
{
"input": {
"customer": {
"id": "cus_xxx",
"email": "[email protected]"
},
"items": [
{ "product": "Widget", "quantity": 2, "price": 1999 }
]
}
}In workflow steps:
{
"config": {
"to": "{{input.customer.email}}",
"subject": "Order for {{input.items.0.product}}"
}
}Input Validation
Define expected input with a JSON schema at the workflow level:
{
"name": "Create Invoice",
"trigger": {
"type": "api"
},
"inputSchema": {
"type": "object",
"required": ["customerId", "amount"],
"properties": {
"customerId": {
"type": "string",
"pattern": "^cus_[a-zA-Z0-9]+$"
},
"amount": {
"type": "number",
"minimum": 100
},
"currency": {
"type": "string",
"enum": ["usd", "eur", "gbp"],
"default": "usd"
}
}
},
"steps": [...]
}If validation fails, the API returns 400 Bad Request:
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Input validation failed",
"details": [
{
"path": ["customerId"],
"message": "Required"
}
]
}
}Idempotency
Prevent duplicate workflow runs with idempotency keys:
curl -X POST http://localhost:3000/api/v1/workflows/{id}/run \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": { "orderId": "order_123" },
"idempotencyKey": "process-order-123"
}'If the same idempotencyKey is used again within 24 hours:
- The API returns the existing run instead of creating a new one
- No duplicate processing occurs
Best practices:
- Use meaningful keys:
process-order-{orderId},invoice-{invoiceId} - Include relevant identifiers from your input
- Keys expire after 24 hours
Checking Run Status
After triggering a workflow, poll for completion:
curl http://localhost:3000/api/v1/runs/{RUN_ID} \
-H "x-api-key: YOUR_API_KEY"async function waitForCompletion(runId, maxWait = 60000) {
const start = Date.now();
while (Date.now() - start < maxWait) {
const response = await fetch(
`http://localhost:3000/api/v1/runs/${runId}`,
{ headers: { 'x-api-key': API_KEY } }
);
const { data } = await response.json();
if (data.status === 'completed') {
return data.output;
}
if (data.status === 'failed') {
throw new Error(data.error?.message || 'Workflow failed');
}
// Wait 1 second before polling again
await new Promise(r => setTimeout(r, 1000));
}
throw new Error('Timeout waiting for workflow');
}Run Status Response
{
"success": true,
"data": {
"id": "run_xyz789...",
"status": "completed",
"input": { "customerId": "cus_xxx" },
"output": { "invoiceId": "inv_123" },
"startedAt": "2024-01-15T10:00:00.000Z",
"completedAt": "2024-01-15T10:00:05.000Z",
"steps": [
{
"id": "create-invoice",
"status": "completed",
"output": { "id": "inv_123" }
}
]
}
}Specifying a Connection
If your workflow uses multiple connections for the same provider, specify which one to use:
{
"input": { ... },
"connectionId": "conn_production_stripe"
}This is useful when:
- You have separate development and production connections
- Different customers use different OAuth connections
- You need to switch between sandbox and live environments
Error Handling
Common Error Responses
Workflow not found (404):
{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "Workflow not found"
}
}Workflow not active (400):
{
"success": false,
"error": {
"code": "INVALID_STATUS",
"message": "Workflow must be active to run. Current status: draft"
}
}Rate limited (429):
{
"success": false,
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded. Limit: 500 requests per minute."
}
}Invalid input (400):
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Input validation failed",
"details": [...]
}
}Rate Limits
API trigger endpoints have these limits:
| Category | Limit |
|---|---|
| Execute | 500 requests/minute per API key |
See Authentication for all rate limit details.
Use Cases
On-Demand Processing
Trigger workflows from your application:
// When user clicks "Generate Report"
await triggerWorkflow('wf_report_generator', {
input: {
reportType: 'monthly',
startDate: '2024-01-01',
endDate: '2024-01-31',
format: 'pdf'
}
});Testing Workflows
Test webhook workflows without setting up actual webhooks:
# Simulate a Stripe webhook
curl -X POST http://localhost:3000/api/v1/workflows/{id}/run \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"input": {
"type": "invoice.paid",
"data": {
"object": {
"id": "in_test",
"amount_paid": 9900
}
}
}
}'Batch Processing
Trigger multiple workflow runs for batch operations:
const customers = await getCustomersToProcess();
const runs = await Promise.all(
customers.map(customer =>
triggerWorkflow('wf_customer_sync', {
input: { customerId: customer.id },
idempotencyKey: `sync-${customer.id}-${today}`
})
)
);
console.log(`Started ${runs.length} sync workflows`);Scheduled via External Scheduler
If you use an external scheduler (cron job, cloud scheduler):
# In your crontab or cloud scheduler
0 9 * * * curl -X POST https://api.example.com/api/v1/workflows/wf_daily_report/run \
-H "x-api-key: $API_KEY" \
-d '{"input":{}}'Complete Example
{
"name": "Process Customer Order",
"description": "Create invoice and send confirmation for customer order",
"trigger": {
"type": "api"
},
"inputSchema": {
"type": "object",
"required": ["customerId", "items"],
"properties": {
"customerId": { "type": "string" },
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"productId": { "type": "string" },
"quantity": { "type": "number", "minimum": 1 },
"price": { "type": "number", "minimum": 0 }
}
}
},
"notes": { "type": "string" }
}
},
"steps": [
{
"id": "calculate-total",
"type": "transform",
"name": "Calculate Order Total",
"config": {
"mapping": {
"subtotal": "{{input.items | map: 'quantity * price' | sum}}",
"tax": "{{input.items | map: 'quantity * price' | sum | times: 0.08}}",
"total": "{{input.items | map: 'quantity * price' | sum | times: 1.08}}"
}
}
},
{
"id": "create-invoice",
"type": "action",
"name": "Create Invoice",
"action": "stripe.createInvoice",
"config": {
"customerId": "{{input.customerId}}",
"amount": "{{steps.calculate-total.output.total}}",
"metadata": {
"source": "api-order"
}
}
},
{
"id": "send-confirmation",
"type": "email",
"name": "Send Confirmation",
"config": {
"action": "send",
"to": "{{input.customerEmail}}",
"subject": "Order Confirmed - Invoice #{{steps.create-invoice.output.number}}",
"body": {
"html": "<p>Thank you for your order!</p><p>Total: ${{steps.calculate-total.output.total}}</p>"
}
}
}
]
}Troubleshooting
Workflow Not Starting
- Check workflow status - Must be
active - Verify API key - Needs
workflows:executescope - Check rate limits - May be rate limited
Input Not Available
- Verify input structure - Check JSON is valid
- Check template syntax - Use
{{input.field}} - Review inputSchema - May be stripping unknown fields
Run Stuck in Pending
- Check Trigger.dev - Ensure dev mode is running
- Verify connections - Required connections must be active
- Review logs - Check for execution errors
Loopfour