Skip to content

Documentation

Providers

The three provider types — inbound webhook, polled outbound request and script — and how events get into RunDis.

A provider is a source of events. Before RunDis can do anything, something has to tell it that something happened, and a provider is the thing that listens.

There are three types, and between them they cover anything that speaks HTTP, and anything a shell script can reach. There is no catalogue of supported services and nothing to request — a provider is a URL and a shape, or a command, not an integration someone has to build for you.

Type Direction Use it when
incoming_webhook The source pushes to RunDis The system can send webhooks
outgoing_request RunDis polls the source The system has an API but cannot push
run_script RunDis runs a script on a schedule Neither of the above fits

Inbound webhooks

An incoming_webhook provider gives you a URL and a way for the sender to prove it is the sender. Point a system at the URL, and every payload it posts becomes an event.

curl -X POST https://app.rundis.io/provider/42/webhook \
  -H 'Authorization: Bearer whs_…' \
  -H 'Content-Type: application/json' \
  -d '{"employee": {"id": "E-2231", "status": "terminated"}}'

Who may post

The provider's Verification setting says how a post is authenticated. There are two kinds.

A secret RunDis issues. The default, and the right choice for a system you control. The secret is issued from the provider page and shown once; only a hash of it is stored, so it cannot be shown again. Rotate secret issues a new one, and the old one stops working at that moment. A provider that has no secret refuses every post. Rotating works from a phone too — the provider's page is part of the phone layout, and the new secret comes with a copy button. A sender presents it either as a bearer token, Authorization: Bearer whs_…, or in an X-RunDis-Secret header — whichever the sending system can set. It is compared in constant time and never appears in the URL.

The sender's own signature. For systems that sign each payload with a secret of their own instead of setting a header you choose. RunDis verifies the signature against a secret in the secret store, which you name on the provider as ${secret.NAME} and give the sender the value of. Four schemes:

Verification What the sender sends What to configure at the sender
GitHub X-Hub-Signature-256: sha256=<hex>, HMAC-SHA256 of the body The repository webhook's Secret, content type application/json
Stripe Stripe-Signature: t=<time>,v1=<hex> over <time>.<body> The endpoint's signing secret (whsec_…)
Slack X-Slack-Signature: v0=<hex> over v0:<time>:<body> The app's signing secret
HMAC-SHA256 in a header sha256=<hex> of the body in a header you name, X-RunDis-Signature by default Whatever the sender calls its shared secret

The value stored in RunDis and the value entered at the sender are the same string. A Stripe or Slack post whose timestamp is more than five minutes from the server's clock is refused. In a signature mode the issued secret is not a way in, and there is nothing to issue: the provider page says which signature it verifies and which secret it reads, and its example shows what the sender's post looks like. Because a signature is over the body, an oversized post to a signature-mode provider is refused as 413 before the signature is checked.

In the GitHub mode the event name and the delivery id are recorded on the message as _headers, so a rule can match on _headers.X-GitHub-Event. No other header is recorded, and in the bearer mode the message is exactly what was sent.

A signature mode needs the secret store, which a deployment enables with RUNDIS_SECRET_KEY; without it, only the issued secret is offered.

Payloads are JSON and at most 1 MiB.

The response is not just an acknowledgement. RunDis ingests the payload, evaluates every trigger attached to the provider, and tells you what each one decided:

{
  "message_id": 118,
  "event_id": 204,
  "provider": "Workday",
  "status": "received",
  "trigger_count": 2,
  "matched_count": 1,
  "trigger_results": [
    {
      "trigger_id": 7,
      "trigger_name": "Employee terminated",
      "matched": true,
      "details": [
        {
          "rule_name": "status is terminated",
          "field": "employee.status",
          "matched": true,
          "reason": "value \"terminated\" equals \"terminated\""
        }
      ]
    }
  ]
}

That response is the fastest debugging loop the product has. You do not have to open the UI to find out why a rule did or did not fire — post a payload and read the reason.

A few responses mean something specific:

Status Meaning
201 Ingested. The body carries the trigger results
400 The body was not JSON, or the provider is not an inbound webhook
401 The credential is missing or wrong: no secret presented or none issued, or the signature does not verify, is stale, or reads a secret the server cannot resolve
403 The provider exists but is disabled
404 No provider with that id
405 The request was not a POST
413 The body is larger than 1 MiB
422 The post came from a RunDis workflow more hops deep than the re-entry cap allows (see below)
429 More posts to this provider than the limit allows; retry after a second (Retry-After: 1)

Posts are rate limited per provider — 50 a second with a burst of 100, per API instance, by default — and the limit is checked before anything else, so a flood costs nothing past it. A 429 says which provider and asks for a second's pause.

Whether an id exists is answered without the credential (the 404); everything else about the provider — its type, whether it is enabled — is not.

The 403 is worth knowing about, because a provider is disabled when you create it. Enable it before you expect anything to happen.

Workflows that post back into RunDis

A workflow's http_webhook or api_call step can post to one of your own inbound webhooks, and that post can fire a trigger and run another workflow, which can post again. That is allowed on purpose — enrich and re-submit is a real pattern — but it is bounded. Every request a run makes carries two headers:

Header Value
X-RunDis-Run The id of the run making the request
X-RunDis-Hop How deep the chain is: one more than the message that fired the run had

A post that carries them is recorded with _hop and _origin_run on the message, beside whatever was sent, so a rule can match on them and a message list shows where a payload came from. A post whose hop is above RUNDIS_WEBHOOK_MAX_HOPS (8 by default) is refused with 422, the step that made it fails with that reason in its output, and the chain ends there. A message from any other sender carries neither field.

Polled outbound requests

An outgoing_request provider works the other way around: RunDis calls the source on a schedule and treats each response as an event.

Setting Description
Endpoint URL The URL to poll
Method GET, POST, PUT, PATCH or DELETE, defaulting to GET
Headers Sent with every request
Poll interval Seconds between polls, defaulting to 60
Auth none, bearer, basic or api_key

Changes take effect without a restart. The polling service re-reads its providers every few seconds, so changing an interval reschedules it, and the URL, headers and credentials are re-read on every tick.

Scripts

A run_script provider covers the systems that have neither a webhook nor a usable API — a directory you have to query with a tool, a file you have to parse, an API that only speaks through a CLI. The script produces the payload and RunDis ingests its output as an event.

Setting Description
Command A shell script, run by /bin/sh
Poll interval Seconds between runs, defaulting to 60
Timeout Seconds the command may run, defaulting to 30; the server caps it (15 minutes by default)

Every poll interval the command runs on the same script runner that runs script actions: on app.rundis.io that is its own short-lived container with nothing of RunDis in it, network access to the internet only, a restricted PATH, curl and jq. What the script prints to stdout is the event:

  • a JSON object is ingested as it is, so {"users": 3} gives a rule the field users to match;
  • anything else — plain text, a JSON array — is wrapped as {"output": "…"}.

Three fields ride along: _exit_code, _ran_at, and _stderr (the first 4 KiB). The event is ingested whatever the exit code: a script may be signalling through it, and a rule can match _exit_code. Only the runner not running the script at all — no runner on the server, a busy queue — produces nothing; the next interval tries again.

# Poll a directory and emit a count. `jq` and `curl` are in the runner image.
curl -s -H "Authorization: Bearer ${secret.DIRECTORY_TOKEN}" \
  https://directory.example.com/api/users?status=disabled \
  | jq '{disabled: length, source: "directory"}'

Because the command is arbitrary code on our infrastructure, it takes the same rules as a script action's command: an admin sets it, and only where the server has a script runner. Where it hasn't, the type is not offered and an existing script provider can be renamed, disabled or removed but its command can't be changed. Renaming, disabling and the interval are member work.

Credentials

Credentials run in two directions. For a polled provider, the auth token RunDis sends is stored as a sensitive field: it is write-only through the API and is never returned in a query. You can replace it; you cannot read it back. For an inbound webhook in the bearer mode, the secret a sender presents is shown once when issued and then kept only as a hash (see above).

Better still, do not put the credential here at all. The auth token, any header value, a script provider's command, and an inbound webhook's signing secret can reference a secret:

${secret.SOURCE_API_TOKEN}

The reference is resolved when the poll runs, when the script runs, or on each post the signature is checked on, and the stored value keeps the reference. One token shared by several providers is then rotated in one place. The endpoint URL is not substituted, so a key that has to live in the URL still goes there directly. A signing secret is the narrowest: the whole value must be one reference, since it is a key rather than text.

Built in: Correlations

Every organisation also has a provider it didn't create: Correlations. RunDis publishes each new correlation as a message from it, so attaching a trigger to it is how you act on a correlation. See Correlation.

It is managed by RunDis. It can't be created, deleted, renamed, disabled or retyped, nothing can post to it, and correlation rules can't use it as a source or target. You can change its description and notes and attach triggers to it.

Names

A provider's name is unique within your organisation, not across RunDis. Another organisation's "Workday" doesn't stop you having one.

What happens next

Ingesting an event is only the first half. Every event is evaluated against the triggers attached to its provider, and those triggers decide whether a workflow runs — see Triggers and rules.

One provider can feed many triggers, and one trigger can listen to many providers. A condition inside a trigger can also be scoped to a single provider, which is what makes a trigger spanning several sources workable.