# Custom actions Extend workflows with actions from third-party or your own apps. Extend workflows beyond Stripe’s built-in actions with custom actions. You can use actions from your own apps or from third-party apps. Users can add custom actions to workflows in the Dashboard by selecting them from the action menu, configuring their parameters, and publishing the workflow. You can also: - [Use available custom actions](https://docs.stripe.com/workflows/custom-actions.md#use-available-custom-actions) - [Build your own custom action](https://docs.stripe.com/workflows/custom-actions.md#build-your-own-custom-action) ## Use available custom actions Some third-party Stripe Apps contain custom actions that you can add to the workflow builder without writing any code. To use these actions, install the app and start using its actions in your workflows. ### Browse custom actions To see what custom actions are available: 1. Go to [Workflows](https://dashboard.stripe.com/workflows) in the Dashboard. 1. Open an existing workflow or create a new one. 1. Click **Add action** to open the action menu. 1. Browse the available options to find actions that fit your use case. Custom actions from installed apps appear alongside built-in Stripe actions. If you don’t see the action you need, check the [Stripe App Marketplace](https://marketplace.stripe.com) for apps that provide workflow actions. ### Install a Stripe App To use a custom action from a third-party app, you need to install the app first: 1. Find the app in the [Stripe App Marketplace](https://marketplace.stripe.com) or through the workflow builder. 1. Click **Install** and follow the prompts to authorize the app on your Stripe account. 1. After installation, the app’s custom actions appear in the workflow builder action menu. ### Complete third-party setup Some custom actions connect to external services (for example, a CRM, email platform, or messaging tool). If the app requires a third-party integration: 1. Create an account with the third-party service if you don’t already have one. 1. Provide API credentials to the third-party service, typically an API key or OAuth connection. 1. Configure the connection by opening the app’s settings in the Stripe Dashboard and entering your third-party credentials. The app stores these securely and uses them when the action runs. Each app’s Marketplace listing describes what external setup is required. Complete this setup before using the action in a live workflow. ### Add the action to your workflow After the app is installed and any third-party setup is complete: 1. Open or create a workflow. 1. Click **Add action** and select the custom action from the action menu. 1. Configure the action’s parameters. The form fields vary depending on the app. 1. Publish the workflow. The custom action runs as part of your workflow like any built-in Stripe action. ## Build your own custom action You can build your own custom action if the available ones don’t cover your use case. A custom action is an extension you create as part of a [Stripe App](https://docs.stripe.com/stripe-apps.md). The extension plugs into the `core.workflows.custom_action` extension point, which defines the contract between your code and the workflow builder. Your action appears in the workflow builder for you. If you publish the app to the App Marketplace, the action is available to anyone who installs your app. ## Required methods The `core.workflows.custom_action` extension point requires two methods: | **Method** | **When it runs** | **Purpose** | | ---------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | `execute` | At runtime, when the workflow runs | Perform your action following the logic you defined (send an email, call an API, create a record) | | `get_form_state` | At configuration time, when a user sets up the action in the Dashboard | Populate dynamic dropdowns, show/hide fields, validate input | ## How custom actions work At configuration time, when a user adds your action to a workflow in the Dashboard: 1. The workflow builder renders a form based on your input schema and UI schema. 1. The builder calls `get_form_state` to populate dynamic dropdowns, show or hide fields, and set initial field states. 1. As the user changes field values, the builder calls `get_form_state` again with the changed field name, and your implementation returns updated options, schemas, and field states. 1. The user fills in the form and saves the workflow. Stripe stores the configured values. At runtime, when the workflow triggers: 1. The workflow engine calls `execute` with the saved configuration values. 1. Your action runs according to the logic you defined (send an email, create a record, call an external API). 1. Your action returns success or failure. The workflow engine handles retries for transient failures. The sections below explain each piece (schemas, dynamic forms, and runtime behavior) in detail. ## Action parameters Each custom action defines an **input schema** (what data the user configures) and a **UI schema** (how the configuration form looks in the workflow builder). ### Input schema A JSON Schema (draft-07) file that defines the data contract. Stripe saves this data when the user configures the action, and passes it to the execute method at runtime. Supported types: - `string` - `boolean` - `integer` - `object` - `array` (array items must be `string`, `boolean`, or `integer`) ```json { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "audience_id": { "type": "string", "title": "Audience", "description": "Select a mailing list" }, "segment_id": { "type": "string", "title": "Segment", "description": "Target a specific segment within the audience (optional)" }, "template_id": { "type": "string", "title": "Email Template", "description": "Select an email template to use" }, "template_variables": { "type": "object", "title": "Template Variables", "description": "Fill in the merge fields for your selected template" }, "message": { "type": "string", "title": "Message", "description": "Custom message body to include in the email" } }, "required": ["audience_id", "template_id"], "additionalProperties": false } ``` Keep the JSON Schema definition simple for fields powered by dynamic behavior (dropdowns, dynamic schemas). The actual options and schemas come from `get_form_state` at configuration time. #### UI schema A JSONForms file that controls layout and dynamic behavior. ```json { "type": "VerticalLayout", "elements": [ { "type": "Control", "scope": "#/properties/audience_id", "options": { "format": "dynamic_select" } }, { "type": "Control", "scope": "#/properties/segment_id", "options": { "format": "dynamic_select" } }, { "type": "Control", "scope": "#/properties/template_id", "options": { "format": "dynamic_select" } }, { "type": "Control", "scope": "#/properties/template_variables", "options": { "format": "dynamic_schema" } }, { "type": "Control", "scope": "#/properties/message", "options": { "multi": true, "template": true } } ] } ``` The `message` field uses `multi` for a multiline text area and `template` to enable template string interpolation, where users can reference workflow data using template syntax. **UI schema format options:** | **Option** | **Description** | | ---------------------------- | --------------------------------------------------------------------------------------------------- | | `"format": "dynamic_select"` | Dropdown whose options are populated by `get_form_state` | | `"format": "dynamic_schema"` | Object whose schema is returned dynamically by `get_form_state` | | `"multi": true` | Renders the field as a multiline text area | | `"template": true` | Enables template string interpolation, allowing users to reference workflow data in the field value | | *(none)* | Standard control rendered from the JSON Schema type | ### Dynamic forms with `get_form_state` The `get_form_state` method powers dynamic form behavior in the workflow builder (dropdown options, dynamic schemas, field states, and value updates). This method is called: - **On initial form load**: `changedField` is `null`. Return the initial configuration for all dynamic fields. - **When a user changes a field value**: `changedField` contains the field name. Return updated configuration based on the new value. **Request** ```ts interface GetFormStateRequest { values: { [fieldName: string]: any }; // Current form field values changedField: string | null; // Which field changed (null on load) } ``` **Response** ```ts interface FieldConfig { options?: Array<{ value: string; label: string }>; // For dynamic_select fields schema?: JSONSchema; // For dynamic_schema fields disabled?: boolean; // Grey out the field hidden?: boolean; // Hide the field entirely warning?: string; // Shows warning message, workflow can still be saved error?: string; // Shows error message, workflow can't be saved } interface GetFormStateResponse { values?: { // Optional — updated field values [fieldName: string]: any; // Omit to leave values unchanged }; config: { // Configuration for each dynamic field [fieldName: string]: FieldConfig; }; } ``` #### Example pattern: cascading dropdowns A user selects an email audience, and the segment dropdown populates with segments from that audience: 1. **Initial load**: Return all audience options, disable segment dropdown (no audience selected yet). 1. **User selects audience**: Return segment options for that audience, enable segment dropdown. 1. **User changes audience**: Clear segment value, return new segment options. ### Runtime behavior #### Timeouts Each call to `execute` has a 20-second timeout. If your endpoint doesn’t respond within 20 seconds, Stripe treats the call as failed and retries it. Design your action to complete within this window. #### Retry behavior Workflows retry failed actions based on the HTTP status code: | **Status** | **Meaning** | **Retried?** | | ---------- | --------------------------------------------- | ------------ | | 200 | Success | No | | 4xx | Permanent failure (bad input, invalid config) | No | | 5xx | Transient failure (service down, timeout) | Yes | Return 4xx for errors that won’t resolve on retry (invalid template ID, malformed input). Return 5xx only for transient issues (external service temporarily unavailable). #### Idempotency Because retries happen automatically, your `execute` implementation must be idempotent. The same action might be called multiple times for the same workflow execution. Use the request `id` field as an idempotency key to deduplicate. For example, check whether you’ve already sent an email for that request ID before sending another. #### You don’t need async processing Workflows runs actions asynchronously and handles retries for you. Your `execute` endpoint doesn’t need to implement its own async job processing. If your work fits within the 20-second timeout, do it synchronously and return success or failure. The workflow engine handles the rest. If you choose to go async (kick off work and return 200 immediately), be aware that you lose the ability to report errors back to the workflow. From the workflow’s perspective, your action returned success. For example, if your action sends an email through an external service and that service is down, the workflow still records the action as successful because your endpoint returned 200. The workflow won’t retry, and you won’t see an error in run details. ### Best practices - **Return config for all dynamic fields on every `get_form_state` call.** The UI replaces config with what you return, so include all fields in every response. You can use `changedField` to scope how you compute the config — for example, skip re-fetching data for fields unrelated to the change — but always return the full config object. On initial load (`changedField: null`), fetch everything. - **Keep `get_form_state` responses fast.** This method runs during form interaction. Target <500ms response times. - **Use `disabled` and `hidden` for dependent fields.** Don’t error when a parent field hasn’t been selected yet, return `{ disabled: true }` instead. - **Clear dependent values when parents change.** Use the `values` return to set dependent fields to `null` (for example, clear segment when audience changes). - **Handle stale saved values.** If a saved value no longer exists in the options, return a `warning` instead of auto-clearing. This lets users see what happened. - **Handle errors per-field.** If a fetch fails for one field (for example, the template service is down), return an `error` on that specific field rather than failing the entire `get_form_state` request. This gives users a better experience — they can still configure the other fields. - **Make `execute` idempotent.** Use the request `id` to deduplicate. - **Use 4xx for permanent failures, 5xx for transient failures.** This controls retry behavior. ## See also - [Set up workflows](https://docs.stripe.com/workflows/set-up.md) - [Use cases](https://docs.stripe.com/workflows/use-cases.md) - [Loop over collections](https://docs.stripe.com/workflows/loops.md) - [Trigger workflows programmatically](https://docs.stripe.com/workflows/programmatic-triggers.md)