Create a custom workflow action
- How-to guide
- 9-min read
You can extend Workflows via custom actions. In a custom action, you implement your integration with any third-party system that allows the user to communicate with other systems. Users can then add your custom action as a task to their workflows. This guide explains how to create a custom action.
In this guide, you'll create a custom action named greeter that:
- Accepts a name and a connection as an input.
- Returns a greeting message using the name given as the input and logs the message in Workflows.
- Shows how to retrieve the connection as an app settings object by its ID.
Create an action
A custom action is part of a Dynatrace app. To create an action, run the following command in the root directory of your app within the terminal:
npx dt-app action create greeter
The command will generate an action named greeter in the /actions directory with some default files and register the action in the app config. When you run dt-app deploy, it will detect the action on the filesystem and deploy it as part of the whole app artifact, regardless of whether the action is registered in the app config.
If you develop an app that only consists of actions and doesn't provide a UI, consider hiding your app in the launcher by setting
app.hiddentotruein yourapp.config.json.
Update Jest config
The earlier command also generates Jest files to test your action. If you use Jest for testing your Dynatrace app, you can update your jest.config.js file in the root directory as follows:
module.exports = {
projects: ['<rootDir>/actions/jest.action.config.js', '<rootDir>/actions/jest.widget.config.js'],
};
Run the action in a workflow
Now that you have updated the app configuration and have all the files, you can deploy the app to run it in a workflow using the following command in the terminal:
npx dt-app deploy
After deployment, do the following:
- Open Workflows and create a new workflow.
- Then, add a task and select the Greeter task from the list.
- Save the workflow and run it.
By running a workflow, you'll end up in the execution monitor. Within there, you can see not only execution details but also the input sent to your action and the result produced by your action. This information is in the respective Detail, Input, and Result tabs. You can find the logs in the dedicated log section at the bottom of the page.
To find the details about building and monitoring workflows, visit Introduction to workflows
| Input Tab | Result Tab |
|---|---|
![]() | ![]() |
Congratulations. You ran a task successfully with your custom action.
Widget interface
The App Toolkit generates a default widget interface for you after you execute the npx dt-app action create command. For the greeter action, you'll find this default widget UI in /actions/greeter.widget.tsx. It has two UI elements:
- Connection: It's an
AutomationConnectionPickercomponent that lets you pick an app setting. The user defines this app setting, which contains information to connect to an external system. It's then sent to the action logic as theconnectionIdproperty. - Name: It's an input element where the user can insert their name. It's also sent to the action logic as a
nameproperty.
Build the widget's input elements with components from @dynatrace/automation-action-components. These components support expression mode, along with autocompletion, expression chips, and the expression editor, which the standard Strato form fields don't provide. Only use standard Strato components for UI interaction where you are absolutely sure no parameterization via expressions is required. This should be the case very seldom.
The input produced by the widget is the payload for the action app function, and the app function's return value is the action's result. Both form a contract with every workflow that already uses the action, so changes to them must stay backwards-compatible. See Safely evolve your action.
This code will result in the following widget UI:
While developing locally, you can mock the connection picker value (My Connection). Go to local development section for app settings to learn more.
Action logic
The App Toolkit generates a default action logic in the /actions directory. This default code handles the connectionId and the name payload sent by the widget interface. You can find the file at /actions/greeter.actions.ts. This file is an app function explicitly for the greeter action.
It's essential to understand the following things:
- The input from the widget interface is available as the
payloadproperty. - The app function's return value becomes the output of the action.
Get app settings
If you look at the code in the file, you'll find that you can get the app settings using getAppSettingsObjectByObjectId from @dynatrace-sdk/client-app-settings-v2 as follows:
const connectionObject = await appSettingsObjectsClient.getAppSettingsObjectByObjectId({
objectId: payload.connectionId,
});
To use await in React components, you need to wrap the asynchronous invocation in an async function. Read more about it in this guide.
It'll give you access to the URL and the token required to access the external system through connectionObject.value.url and connectionObject.value.token, respectively. You can use them to access the external system in your app function.
The generate action command automatically includes the app-settings:objects:read scope in your app config, which is essential for operating the getAppSettingsObjectByObjectId function. There's no need for manual scope addition, as the command ensures your app has correct permissions for accessing settings objects.
It's essential to understand that there are different types of fields in settings. Especially for any credentials like token, the "type": "secret" should be used.
Log messages
You can log messages for the end user using the userLogger function exported by @dynatrace-sdk/automation-action-utils as follows:
userLogger.info(`Hello ${payload.name}! Have a great day!`);
These logs appear in the task's logs in the Workflow for every execution.
An action has two logging channels with different audiences, so use them deliberately:
userLoggeris visible to the user in the execution monitor. Because the user decides whether to share it, it may carry data that helps them debug, such as sanitized URLs, response bodies with sensitive fields redacted, or partial identifiers. On failure, write a single structured block here - the request method and URL (without headers), the response status, and the error message - so the user can copy it, redact anything sensitive, and hand it to support in one step instead of round-tripping for details.console.*feeds Dynatrace platform self-monitoring. Follow general logging best practices, especially never log anything containing secrets or personal data.
Regardless of the channel, never log sensitive data, such as Authorization header values or tokens.
The log size is limited to 1 MB.
Debug the action
Workflows offer a dev-helper tool that helps you debug action widgets.

You can access the dev helper via the links displayed by the dt-app on your terminal after executing the npx dt-app dev command. The link points to the Automations dev-helper app and its URL follows this style: https://<your-environment>/ui/apps/dynatrace.automations/dev-helper?src=<widget-url>.
The total size of a custom workflow action's result and log data must not exceed 6 MB. A larger response will cause the action to fail.
Show sample results
You can show a sample result for your action. Users can look at this sample and figure out what the action output would look like.
The App Toolkit creates a greeter.sample-result.json file in the /assets directory for our greeter action. Open this file and add the sample JSON result.
Make the sample mirror a real, successful result: use the exact shape your action returns, with realistic values. Users and downstream tasks rely on it to understand the output, so an accurate sample is what lets them build on your action. Don't include personal data or secrets, because the sample is visible at design time.
End an action early
If you want to exit from your action at any point, you can use the UnsuccessfulActionError error class from @dynatrace-sdk/automation-action-utils/actions as follows:
throw new UnsuccessfulActionError('A message why the action failed');
This will result in the action's failure with the given message.
Validate expression values
For actions with an input that's some sort of syntax for a third party system (for example, a Kubernetes manifest, an SQL query, or similar), you want to protect your action from injection attacks. You can, therefore, control the values against which to evaluate an expression. To prevent any unwanted input, specify an expressionValidation in your action manifest to validate the expression value against a pattern.
For example, the app evaluates any expression in the name field against uppercase letters.
// Excerpt of app.config.json
{
...
"actions": [
{
"name": "greeter",
"title": "Greeter",
"expressionValidation": {"name": {"pattern": "[A-Z]+"}}
}
]
}
A violation such a validation at runtime shall fail the task.
Best practices and conventions
The steps above show how to build a working action. This section covers the conventions that keep an action maintainable: how to structure it, where to put your integration logic, how to validate and time out external calls, and how to evolve the action without breaking existing workflows.
Validate every input in the action
The AutomationEngine resolves the expressions in a task's input, but it does not validate the resolved values. It is the responsibility of the action itself to be the source of truth for input validation: validate every input your action receives before you use it (for example, with zod).
A workflow can be authored outside the Workflows UI, for example through the API or Terraform, so you must never rely on the widget to enforce a contract on the input configuration.
The role of the action widget - an input helper only
The widget is a design-time helper for users, not a trust boundary. The action must validate all input itself — the widget can't enforce a contract on what values it receives.
Keep the following in mind when building the widget:
- Users often use expressions rather than static values, and workflows can be authored outside the Workflows UI entirely (for example, through the API or Terraform), so the widget is bypassed.
- Don't require a live connection to the target system for configuration. A connection field is often filled by an expression that can't be resolved at design time, so the widget must remain usable even when the remote system is unreachable.
- Don't replicate or compete with the target system's own UI. Focus on what the action needs.
- Use components from
@dynatrace/automation-action-componentsso users can switch fields to expression mode.
Safely evolve your action
Existing workflows are not migrated when you change an action, so an action's input and result form a contract with every workflow that already uses it. Make backwards-compatible changes only:
- Inputs—don't add new required inputs. Add new inputs as optional and apply a default in the action logic, so workflows created before the change keep working.
- Results—only add to the result. Renaming or removing a result property silently breaks downstream tasks that reference it, so keep the existing properties and add alongside them. Update the sample result whenever the shape changes.
A breaking change forces every existing workflow that uses the action to be updated by hand - until then the action and the workflows will fail.
Two kinds of workflow actions
A custom action typically integrates one of two things:
- A third-party service reached over its HTTP API, such as Slack, Jira, or ServiceNow. The action resolves a connection into credentials and calls the remote API with
fetch. All knowledge of that API is portable and independent of the Dynatrace platform. - A Dynatrace platform service reached through an official TypeScript SDK client (
@dynatrace-sdk/client-*), for example to run a DQL query or create a problem. Authentication flows from the workflow's actor context, so there's no third-party connection to resolve. Check the SDK for TypeScript to see whether a client exists for the service you want to call.
Both shapes share the same skeleton: validate the input, do the work, surface any failure with a clear message (see End an action early), and log. They differ only in the "do the work" layer. A single action can also mix both, for example calling a platform service and a third-party API.
The greeter action in this guide is a third-party-style action. The conventions below apply to both shapes unless stated otherwise.
Prefer fetch over custom HTTP client hierarchies
For a third-party action that calls only one to three endpoints, fetch together with AbortSignal.timeout() covers what you need. Avoid building elaborate client class hierarchies, such as custom encoders, generic call factories, or response parsers, around fetch. They add maintenance cost and make the action harder to pick up later. Document any API quirks in your project instead of encoding them in a class hierarchy.
Runtime of action determines throughput
Don't rely on the full 120-second app function budget. Strive to complete as fast as possible — a target under 5 seconds is a good baseline. Faster actions allow workflows to run more concurrent executions.
Any call to an external system can hang. When you use fetch, always pass an AbortSignal.timeout() sized to the endpoint you call:
const response = await fetch(url, {
signal: AbortSignal.timeout(10_000),
});
Keep the timeout well under the app function's runtime budget, so your action can still handle the error, and log a useful message. See Runtime limitations for the app function time limit.
Separate business logic from platform plumbing
Keep the code that knows the integrated API separate from the Dynatrace platform plumbing, that is, the connection, the action entry point, and the widget. A common convention is to place this integration logic in a shared/api/ layer that the action imports.
For a third-party integration, this separation lets you:
- Run and iterate on the integration logic locally, without deploying the app.
- Unit-test it with no platform dependencies.
- Keep the action itself thin, so it stays simple to read and support.
For a platform-service action, the logic layer imports the SDK client, so it isn't free of platform dependencies. You mock the client in unit tests instead. You can still run it locally: every @dynatrace-sdk/client-* is also exported as a class that accepts a custom HTTP client, so you can point it at your environment during development.
Actions are atomic building blocks
Users can place any action anywhere in a workflow graph, thus don't assume a specific trigger type, trigger configuration, or the presence of any preceding task.
Don't embed hidden input fields with hardcoded expression values. If your action needs access to workflow or execution context (for example, send Slack message wants to continue a Slack thread opened earlier in the same execution), use the utilities provided by @dynatrace-sdk/automation-action-utils.
Action/Task testing in the UI
The widget runs only in the context of user interaction and cannot resolve expressions, so an action can't own the responsibility for single-task testing. On-demand task execution is handled by the Workflows app, not by the action.
Connection access
Action executions run under the actor configured on the workflow, so connection resolution and app settings access happen in that user's context. Don't request or depend on the app-settings:objects:admin add-on permission for settings objects — use the standard settings access model. The admin permission is reserved for administrative tasks in the connection management UI, not for runtime use within an action.
Summary
You learned how to create a custom action with the action widget UI and action function to implement its functionality. For further information about the Workflows and AutomationEngine, have a look at the user documentation.

