Skip to content

Documentation

Actions

The five action executors — HTTP webhook, email, Slack message, script and API call — and every configuration field each one accepts.

An action is a reusable executor with a name and a configuration. Steps point at actions; an action configured once can be called by as many steps as you like, in as many workflows.

Every action has a type, and the type decides which executor runs and which configuration fields mean anything. There are five.

Type What it does
http_webhook Sends an HTTP request, optionally with a templated body
email Sends an email
slack_message Posts to a Slack incoming webhook
script Runs a shell command in its own short-lived container
api_call Makes an authenticated API call

A step passes input to its action: the values it mapped from the trigger's event or from an upstream step. What the executor does with that input differs by type and is described below.

Templates

Email subjects and bodies, Slack message text and webhook body templates are Go text/templates. Each can read:

In the template Is
{{ .trigger.… }} The payload that fired the trigger. For a correlation, the correlation message
{{ .steps.{step name}.… }} An earlier step's output, reached into by path when it is JSON
{{ .{name} }} A value the step mapped into name

So a Slack step needs no mappings to say {{ .trigger.employee.name }} left on {{ .trigger.effective_date }}. If a step maps a value into a name of its own called trigger or steps, that mapping wins.

Two functions help: json renders a value as JSON ({{ json .trigger.matchedFields }}), and default supplies a fallback ({{ default "unknown" .trigger.manager }}).

What to know:

  • In email and Slack, a missing value fails the step. {{ .trigger.nope }} stops the step with an error naming the key, rather than sending a message that says <no value>. The step's failure policy then applies. Webhook bodies keep rendering <no value>, as they always have.
  • Slack escapes the data, not your template. &, < and > in payload values arrive as text, so an event containing <!channel> can't mention a channel. Links and mentions you write yourself still work.
  • Email subjects are one line. Line breaks in rendered data are replaced with spaces, so a payload can't add a header.
  • Secrets are never read from data. ${secret.NAME} in an action's configuration is resolved before rendering; the same text inside a payload is sent as text.
  • To write a literal {{, write {{"{{"}}.

http_webhook

Makes HTTP requests to configured URLs with templated payloads.

Field Type Required Description
url string yes Target URL
method string no HTTP method, defaults to POST
headers map no Additional request headers
body_template string no Go text/template for the request body. body is accepted too
timeout_seconds number no Request timeout, defaults to 30

With a body_template, the template renders the body, so {{ .user_email }} renders the value the step mapped into user_email and {{ .trigger.… }} reads the event (see Templates). Without one, the input is sent as JSON. Either way the request carries Content-Type: application/json, and X-RunDis-Run and X-RunDis-Hop naming the run it belongs to — which is how a post back into one of your own inbound webhooks is recognised and bounded. A configured header of the same name does not override them.

A response status of 400 or above fails the step. The response body is recorded as the step's output, truncated at 1 MB.

email

Sends emails via SMTP.

Field Type Required Description
to list of strings yes Recipient email addresses. A comma-separated string, or the key recipients, is accepted too
subject string yes Email subject line, a template
body string yes Email body (plain text), a template
cc list of strings no CC recipients. A comma-separated string is accepted too

The subject and body are templates (see Templates), and the body is sent as plain text. On success the step's output names the recipients, so run history shows where a message actually went:

email sent to ops@example.com, manager@example.com

slack_message

Posts messages to Slack via an incoming webhook URL.

Field Type Required Description
webhook_url string yes Slack incoming webhook URL
text string yes Message text, a template. message is accepted too
channel string no Override channel
username string no Override bot username
icon_emoji string no Override bot icon

The text is a template (see Templates). The three overrides are honoured only if the Slack webhook you created permits them; that is Slack's rule, not ours.

script

Runs a shell command in a container of its own. On hosted RunDis each run is a short-lived Kubernetes Job from a small image (BusyBox, curl, jq, CA certificates) that holds nothing of RunDis: no credentials, no service-account token, a read-only file system apart from /work and /tmp, and network access to the internet only — not to RunDis's own database, event bus or cluster, and not to the nodes or RunDis's own public endpoint. The Jobs run in a namespace of their own under Kubernetes' restricted Pod Security profile, where RunDis itself can read only script pods' logs. A container is not a virtual machine; it is a separate, unprivileged process tree, and that is what we claim.

Only admins can create a script action or edit its command: it is arbitrary code run from RunDis's address. Runs start a few seconds after the step does (a pod has to be scheduled), and at most a handful run at once; further runs wait briefly and then fail as busy.

Field Type Required Description
command string yes The shell command or script to execute
timeout_seconds number no Max execution time, defaults to 30
working_dir string no Working directory for the script
env map no Environment variables to set

The step's input arrives as environment variables, uppercased and prefixed with INPUT_ — a step that maps user_email exposes $INPUT_USER_EMAIL.

These limits are not configurable.

  • Runs under /bin/sh with a restricted PATH of /usr/local/bin:/usr/bin:/bin; working_dir must be /work or under it
  • The timeout is enforced by the runtime, not by the script
  • The process gets its own process group, so a timeout kills its children too

Standard output becomes the step's output, capped at 1 MB, with anything on standard error appended after a STDERR: marker. The command's real exit code is preserved, so a non-zero exit fails the step.

api_call

Makes authenticated API calls to provider endpoints. Like http_webhook, every request carries X-RunDis-Run and X-RunDis-Hop.

Field Type Required Description
url string yes API endpoint URL
method string no HTTP method, defaults to GET
headers map no Additional request headers
body map no JSON request body
auth_type string no bearer, basic, or api_key
auth_token string no The credential, or ${secret.NAME} to reference a secret
api_key_header string no Header name for api_key auth, defaults to X-API-Key
timeout_seconds number no Request timeout, defaults to 30

The three authentication modes each read auth_token and do something different with it:

  • bearer sends it as Authorization: Bearer <token>
  • basic base64-encodes it as given, so supply it already in user:password form
  • api_key sends it in the header named by api_key_header

Prefer a secret over the credential itself. Writing ${secret.STRIPE_API_KEY} here keeps the configuration readable by anyone who needs to understand the action without handing them the token — and a credential written directly is redacted when the configuration is read back, so it cannot be retrieved afterwards anyway.

For POST, PUT and PATCH with no body configured, the step's input is sent as the JSON body instead.

Choosing between http_webhook and api_call

They overlap. The distinction worth drawing is that http_webhook is built for sending — it defaults to POST and gives you a template for shaping the payload — while api_call is built for calling — it defaults to GET and handles authentication for you.

If you are pushing data somewhere and need control over the body, use http_webhook. If you are talking to an authenticated API, use api_call.