Loopfour

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
      }
    ]
  }
}
FieldTypeRequiredDescription
idstringYesUnique step identifier
typestringYesMust be "action"
namestringYesHuman-readable name
actionstringYesAction identifier (e.g., stripe.createInvoice)
configobjectYesAction-specific configuration
retryConfigobjectNoRetry configuration
errorHandlingobjectNoError handling strategy
timeoutMsnumberNoTimeout in milliseconds

Action Format

Actions follow the pattern {connector}.{actionName}:

stripe.createInvoice
salesforce.updateOpportunity
hubspot.createContact
quickbooks.listCustomers
slack.sendMessage

Available Connectors

ConnectorDescriptionExample Actions
stripePayment processingcreateInvoice, getCustomer, createSubscription
salesforceCRM operationsgetAccount, updateOpportunity, query
hubspotMarketing/CRMcreateContact, getDeal, searchContacts
quickbooksAccountingcreateInvoice, getCustomer, createPayment
pandadocDocument signingcreateDocument, sendDocument, getStatus
netsuiteERP operationscreateCustomer, createInvoice, suiteql
slackMessagingsendMessage, openModal, addReaction
emailEmail sendingsend, send_template, schedule
apiCall any HTTP endpointrequest

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
  }
}
FieldTypeDescription
urlstringAbsolute http(s) URL. Templates are resolved before the call.
methodstringGET, POST, PUT, PATCH, or DELETE (default GET)
paramsjsonQuery parameters, as an object or a [{Key, Value}] table
headersjsonRequest headers, same shapes. Use {{secrets.NAME}} for credentials.
bodyjsonObject (sent as JSON, sets Content-Type) or a raw string
responseFormatstringauto (default), json, csv, text, pdf, or binary
csvDelimiterstringDelimiter for csv parsing (default ,)
timeoutnumberMilliseconds; default 300000, capped at 600000
retriesnumberRetry attempts for timeouts, 408, 429 and 5xx (default 0)
retryDelayMsnumberInitial backoff, doubled each attempt (default 500)
retryMaxDelayMsnumberBackoff ceiling (default 30000)
retryNonIdempotentbooleanAllow retries for POST/PATCH (default false)
maxResponseBytesnumberRefuse a larger response (default 20 MB)
failOnErrorbooleanThrow 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

ContextExampleDescription
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
  }
}
FieldTypeDescription
maxAttemptsnumberMaximum retry attempts (1-10)
initialDelayMsnumberInitial delay in ms (min: 100)
maxDelayMsnumberMaximum delay in ms (min: 1000)
backoffMultipliernumberExponential 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"
    }
  }
}
StrategyBehavior
failStop workflow, mark run as failed (default)
continueLog error, continue with fallback output
retryRetry 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.

Next Steps

On this page