Action Steps
Execute integration actions in workflows
Action Steps
Action steps execute operations on external services through connectors. They are the primary way to interact with integrated systems like Stripe, Salesforce, HubSpot, QuickBooks, and more.
Configuration
{
"id": "create-invoice",
"type": "action",
"name": "Create Stripe Invoice",
"action": "stripe.createInvoice",
"config": {
"customerId": "{{input.customer_id}}",
"items": [
{
"price": "price_xxx",
"quantity": 1
}
]
}
}| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique step identifier |
type | string | Yes | Must be "action" |
name | string | Yes | Human-readable name |
action | string | Yes | Action identifier (e.g., stripe.createInvoice) |
config | object | Yes | Action-specific configuration |
retryConfig | object | No | Retry configuration |
errorHandling | object | No | Error handling strategy |
timeoutMs | number | No | Timeout in milliseconds |
Action Format
Actions follow the pattern {connector}.{actionName}:
stripe.createInvoice
salesforce.updateOpportunity
hubspot.createContact
quickbooks.listCustomers
slack.sendMessageAvailable Connectors
| Connector | Description | Example Actions |
|---|---|---|
stripe | Payment processing | createInvoice, getCustomer, createSubscription |
salesforce | CRM operations | getAccount, updateOpportunity, query |
hubspot | Marketing/CRM | createContact, getDeal, searchContacts |
quickbooks | Accounting | createInvoice, getCustomer, createPayment |
pandadoc | Document signing | createDocument, sendDocument, getStatus |
netsuite | ERP operations | createCustomer, createInvoice, suiteql |
slack | Messaging | sendMessage, openModal, addReaction |
email | Email sending | send, send_template, schedule |
api | Call any HTTP endpoint | request |
See Integrations for complete action documentation per connector.
API Request Block
When no connector covers the service you need, the core api block calls any HTTP
endpoint directly.
{
"id": "fetch_report",
"type": "action",
"action": "api.request",
"config": {
"url": "https://api.example.com/v1/reports",
"method": "POST",
"headers": [{ "Key": "Authorization", "Value": "Bearer {{secrets.EXAMPLE_API_KEY}}" }],
"params": [{ "Key": "format", "Value": "detailed" }],
"body": { "startDate": "{{input.startDate}}", "endDate": "{{input.endDate}}" },
"responseFormat": "auto",
"retries": 3,
"retryDelayMs": 500
}
}| Field | Type | Description |
|---|---|---|
url | string | Absolute http(s) URL. Templates are resolved before the call. |
method | string | GET, POST, PUT, PATCH, or DELETE (default GET) |
params | json | Query parameters, as an object or a [{Key, Value}] table |
headers | json | Request headers, same shapes. Use {{secrets.NAME}} for credentials. |
body | json | Object (sent as JSON, sets Content-Type) or a raw string |
responseFormat | string | auto (default), json, csv, text, pdf, or binary |
csvDelimiter | string | Delimiter for csv parsing (default ,) |
timeout | number | Milliseconds; default 300000, capped at 600000 |
retries | number | Retry attempts for timeouts, 408, 429 and 5xx (default 0) |
retryDelayMs | number | Initial backoff, doubled each attempt (default 500) |
retryMaxDelayMs | number | Backoff ceiling (default 30000) |
retryNonIdempotent | boolean | Allow retries for POST/PATCH (default false) |
maxResponseBytes | number | Refuse a larger response (default 20 MB) |
failOnError | boolean | Throw on non-2xx (default true) |
Outputs: data (parsed body), status, headers, ok, contentType, sizeBytes,
plus text for textual formats, rows/columns for CSV, and base64 content for
pdf/binary — so a downloaded file can flow straight into a workspace_files,
data_table, transform, or agent step.
POST and PATCH are not retried unless retryNonIdempotent is set, because a
retry can duplicate a side effect. Requests to loopback and private-network addresses
(including cloud metadata endpoints) are refused unless the deployment sets
ALLOW_PRIVATE_NETWORK_REQUESTS=true.
Template Variables
Use template syntax to reference input and previous step outputs:
{
"id": "update-contact",
"type": "action",
"action": "hubspot.updateContact",
"config": {
"contactId": "{{steps.lookup.output.id}}",
"properties": {
"company": "{{input.company_name}}",
"lifecyclestage": "customer",
"last_order_date": "{{now}}"
}
}
}Available Template Contexts
| Context | Example | Description |
|---|---|---|
input | {{input.customer_id}} | Workflow trigger input |
steps | {{steps.step-id.output.field}} | Previous step output |
variables | {{variables.taxRate}} | Workflow variables |
env | {{env.API_URL}} | Environment variables |
now | {{now}} | Current ISO timestamp |
Nested Access
{
"config": {
"email": "{{input.customer.contact.email}}",
"amount": "{{steps.calculate.output.totals.grandTotal}}"
}
}Error Handling
Retry Configuration
Configure automatic retries for transient failures:
{
"id": "api-call",
"type": "action",
"action": "stripe.createInvoice",
"config": { ... },
"retryConfig": {
"maxAttempts": 3,
"initialDelayMs": 1000,
"maxDelayMs": 30000,
"backoffMultiplier": 2
}
}| Field | Type | Description |
|---|---|---|
maxAttempts | number | Maximum retry attempts (1-10) |
initialDelayMs | number | Initial delay in ms (min: 100) |
maxDelayMs | number | Maximum delay in ms (min: 1000) |
backoffMultiplier | number | Exponential backoff multiplier (1-10) |
retryConfig is accepted by the step schema but is not yet honoured by the
execution engine, and errorHandling.strategy: "retry" re-raises to the task
runner rather than retrying the step. For real per-step retries today, use the
api block's own retries fields (above).
Error Handling Strategy
{
"id": "optional-sync",
"type": "action",
"action": "hubspot.updateContact",
"config": { ... },
"errorHandling": {
"strategy": "continue",
"fallback": {
"synced": false,
"error": "Sync failed"
}
}
}| Strategy | Behavior |
|---|---|
fail | Stop workflow, mark run as failed (default) |
continue | Log error, continue with fallback output |
retry | Retry with retryConfig settings |
Timeout
{
"id": "slow-api",
"type": "action",
"action": "netsuite.suiteql",
"config": { ... },
"timeoutMs": 60000
}Output
Action outputs are stored and available to subsequent steps:
// Access in later steps
{{steps.create-invoice.output.id}}
{{steps.create-invoice.output.number}}
{{steps.create-invoice.output.status}}
{{steps.create-invoice.output.amount_due}}Checking Output Existence
{
"id": "check-result",
"type": "condition",
"config": {
"conditions": {
"left": "{{steps.create-invoice.output.id}}",
"operator": "exists"
},
"then": ["send-notification"],
"else": ["handle-error"]
}
}Common Patterns
Create Then Update
{
"steps": [
{
"id": "create-customer",
"type": "action",
"action": "stripe.createCustomer",
"config": {
"email": "{{input.email}}",
"name": "{{input.name}}"
}
},
{
"id": "create-invoice",
"type": "action",
"action": "stripe.createInvoice",
"config": {
"customer": "{{steps.create-customer.output.id}}",
"auto_advance": true
}
}
]
}Conditional Action
{
"steps": [
{
"id": "check-exists",
"type": "action",
"action": "hubspot.searchContacts",
"config": {
"filterGroups": [{
"filters": [{
"propertyName": "email",
"operator": "EQ",
"value": "{{input.email}}"
}]
}]
}
},
{
"id": "route-action",
"type": "condition",
"config": {
"conditions": {
"left": "{{steps.check-exists.output.total}}",
"operator": "gt",
"right": "0"
},
"then": ["update-contact"],
"else": ["create-contact"]
}
}
]
}With Fallback
{
"id": "sync-to-crm",
"type": "action",
"action": "salesforce.updateAccount",
"config": {
"id": "{{input.sf_account_id}}",
"fields": { "Website": "{{input.website}}" }
},
"errorHandling": {
"strategy": "continue",
"fallback": {
"synced": false,
"syncError": "Failed to update Salesforce"
}
}
}Connector-Specific Examples
Stripe
{
"id": "create-payment",
"type": "action",
"action": "stripe.createPaymentIntent",
"config": {
"amount": "{{input.amount}}",
"currency": "usd",
"customer": "{{input.stripe_customer_id}}",
"metadata": {
"order_id": "{{input.order_id}}"
}
}
}Salesforce
{
"id": "query-accounts",
"type": "action",
"action": "salesforce.query",
"config": {
"soql": "SELECT Id, Name, Website FROM Account WHERE Type = 'Customer' LIMIT 100"
}
}Slack
{
"id": "notify-team",
"type": "action",
"action": "slack.sendMessage",
"config": {
"channel": "#sales-notifications",
"text": "New deal closed: {{input.deal_name}} for ${{input.amount}}"
}
}Troubleshooting
Action Not Found
{
"error": {
"code": "ACTION_NOT_FOUND",
"message": "Action 'stripe.unknownAction' not found"
}
}Verify the action name matches the connector's available actions.
Connection Required
{
"error": {
"code": "CONNECTION_REQUIRED",
"message": "No active connection found for provider 'salesforce'"
}
}Ensure you have an active OAuth connection for the connector.
Rate Limited
{
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded for provider"
}
}Add retry configuration with exponential backoff.
Loopfour