Action triggers

Actions fire one of the following trigger types when triggered. The trigger does the actual work of dispatching to an external system and tracking the outcome.

TypeStatusDescription
webhookShippedPOST to any HTTP endpoint with retry + jitter. Callback signals completion.
pull-requestShippedOpen a GitHub PR via the Git Data API; auto-completes on merge or close.
terraform-cloudShippedFire a TFC / HCP Terraform run natively. Polls until terminal state and transitions the ActionRun on completion.
atmos-workflowShippedInvoke an Atmos workflow via a customer-deployed runner. HTTP-POSTs a structured payload to the customer's runner; the runner clones their Atmos repo, runs atmos workflow <name> -s <stack>, and POSTs back to Terrantula's callback.
atlantisShippedTalk to a customer-hosted Atlantis instance via its API. Two modes: pull-request (default) and api-dispatch.

For interpolation syntax in trigger fields, see Interpolation.


Webhook trigger

POSTs (or GETs / PUTs / etc.) to an HTTP endpoint. The external workflow does the actual provisioning and POSTs back to {{ run.callbackUrl }} with Authorization: Bearer {{ run.callbackToken }} to signal completion.

trigger:
  type: webhook
  url: https://workflows.example.com/onboard
  method: POST                      # default POST; GET, PUT, PATCH, DELETE supported
  auth:
    type: bearer                    # bearer | basic | header | jwt
    token: "{{ secrets.api-token }}"
  headers:                          # optional, supports interpolation
    X-Tenant: "{{ parameters.tenant_id }}"
  payload:                          # JSON body, interpolated
    customer_id: "{{ entity.id }}"
    callback_url: "{{ run.callbackUrl }}"
    callback_token: "{{ run.callbackToken }}"
  retry:                            # optional — defaults: 3 attempts, exponential backoff, 1000ms initial
    maxAttempts: 3
    backoff: exponential            # exponential | fixed
    initialDelayMs: 1000

Auth types. bearer (Authorization: Bearer …), basic (Authorization: Basic base64(user:pass)), header (custom header name + value), jwt (signs a JWT with the named secret and includes claims).

Retry behavior. Retries on 5xx, 429, and network errors with exponential backoff + jitter. 4xx (except 429) fails immediately — retrying a misconfigured URL won't help. Delivery failures are distinguished from callback-reported failures via a [delivery] prefix on the ActionRun's error field.

Completion. The external workflow POSTs to {{ run.callbackUrl }} with Authorization: Bearer {{ run.callbackToken }} and a JSON body indicating success or failure. The ActionRun transitions to succeeded or failed, and the entity / relationship transitions to its onSuccess or onFailure state.

If the callback never arrives, the reaper transitions the ActionRun to failed after the Action's timeout (defaults to 60 minutes if unset).


Pull-request trigger

Opens a GitHub PR and waits for a human to merge or close it. The branch, commits, title, body, files, labels, and reviewers all support interpolation.

trigger:
  type: pull-request
  repo: my-org/infrastructure
  auth:
    type: token
    token: "{{ secrets.github-token }}"
  title: "Onboard {{ entity.properties.companyName }} → {{ recommendations.targetCluster.name }}"
  body: |
    Triggered by Terrantula ActionRun `{{ run.id }}`
    Cluster: {{ recommendations.targetCluster.name }}
    Region: {{ recommendations.targetCluster.properties.region }}
  head: "terrantula/onboard-{{ entity.id }}"
  base: main                        # default 'main'
  files:                            # at least one file required
    - path: "tenants/{{ entity.id }}.yaml"
      content: |
        apiVersion: v1
        kind: Tenant
        metadata:
          name: "{{ entity.properties.companyName }}"
  labels:                           # optional
    - terrantula
    - onboarding
  reviewers:                        # optional, GitHub usernames
    - "{{ parameters.approver }}"
  teamReviewers:                    # optional, team slugs
    - platform-leads
  webhookSecret: "{{ secrets.github-webhook-secret }}"  # for auto-completion

How the PR flow works

When the action fires:

  1. Worker creates the branch from base via the Git Data API
  2. Commits all files to that branch
  3. Opens a PR with the templated title, body, labels, and reviewers
  4. Stores { prNumber, prUrl, repo } in the ActionRun's metadata field
  5. Transitions ActionRun to running

When the PR closes:

  • Merged → ActionRun transitions to succeeded; entity/relationship transitions to onSuccess
  • Closed without merge → ActionRun transitions to failed; entity/relationship transitions to onFailure

Auto-completion via GitHub webhook

For automatic state transitions, set webhookSecret and configure a GitHub webhook on the repo pointing to POST /webhooks/github with that secret. Terrantula verifies X-Hub-Signature-256 per-PR using the action-specific secret stored in Terrantula Secrets — there is no global webhook configuration. Different repos can use different secrets without coordination.

If webhookSecret is omitted, the PR will still be created — but Terrantula won't auto-complete the run. State transitions then need to happen manually via the entity/relationship API or via a GitHub Actions workflow that POSTs to the callback URL. {{ run.id }} (the ActionRun UUID — safe to embed anywhere) can be put in the PR body for reference.

If neither auto-completion nor manual transition happens, the reaper will time the run out after the Action's timeout.

Why PRs?

For organizations with siloed engineering teams, the PR-based approval pattern is the critical feature: a requesting team triggers an action, the owning team gets a PR in their own repo in their normal review queue, their existing pipeline handles provisioning, and Terrantula tracks the outcome. No new tools for the owning team. No process changes for either side.


Terraform Cloud trigger

Fires a Terraform Cloud / HCP Terraform run against a pre-existing workspace. Terrantula never executes terraform — TFC runs the plan/apply, Terrantula tracks the outcome.

trigger:
  type: terraform-cloud
  organization: my-org
  # Exactly one of workspaceName / workspaceId must be set.
  workspaceName: tenant-onboard      # resolved to a workspace ID at dispatch
  # workspaceId: ws-abc123           # alternative: pass the ID directly
  apiToken: "{{ secrets.tfc-api-token }}"
  apiBaseUrl: https://app.terraform.io   # override for HCP private regions
  configSource:                       # informational; TFC pulls config from
    type: vcs                         # the workspace's existing VCS settings
    repo: my-org/tenant-terraform
    branch: main
  variables:                          # run-scoped Terraform variables (HCL literals)
    customer_id: "{{ parameters.customer_id }}"
    region:      "{{ recommendations.target-cluster.properties.region }}"
  autoApply: false                    # default false — plan-only run
  waitForCompletion: true             # default true — Terrantula polls until terminal
  pollIntervalSeconds: 10             # default 10 (range 2–60)
  message: "Onboarding {{ parameters.customer_id }}"

How the run flow works

When the action fires:

  1. Worker resolves workspaceName to a workspace ID via GET /api/v2/organizations/:org/workspaces/:name (skipped when workspaceId is set directly).
  2. Worker POSTs /api/v2/runs with run-scoped variables (JSON-stringified as HCL literal strings), the auto-apply flag, an optional message, and the workspace relationship.
  3. Worker stores { runId, runUrl, workspaceId, workspaceName, organization, status } in the ActionRun's metadata field.
  4. If waitForCompletion=true (default), worker polls GET /api/v2/runs/:id every pollIntervalSeconds. The action's timeout (default 60min) is the upper bound — the reaper marks the run failed if TFC stalls.
  5. Terminal status mapping:
    • Success: applied, planned_and_finished, planned_and_saved, planned, policy_checked → ActionRun succeeded.
    • Failure: errored, discarded, canceled, force_canceled, policy_soft_failed → ActionRun failed with Terraform Cloud run '<status>' — see <runUrl> as the error.

autoApply: false and theplanned status

When autoApply: false (the default), TFC completes the plan and waits for a human operator to click "Confirm & Apply" before applying. Terrantula considers the Action complete when the TFC run reaches planned — meaning the plan has finished but the infrastructure has not yet been provisioned. The entity will transition to its onSuccess state (e.g., active) at this point, before TFC has actually applied any changes.

This is intentional plan-only semantics: Terrantula's job is to track that the provisioning workflow was initiated and the plan succeeded. If your OnboardTenant Action requires the tenant infrastructure to actually exist before the entity transitions to active, set autoApply: true. With autoApply: true, TFC applies immediately after planning and the entity only transitions once the run reaches applied.

planned_and_finished (no changes required) and planned_and_saved are genuinely terminal — they mean TFC found nothing to do or saved the plan for later. Both count as success.

Run-scoped variable sensitivity

Variable values passed via variables: { ... } are sent to TFC with sensitive: false and will appear as cleartext in the TFC UI's run details panel — including values resolved from Terrantula Secrets.

Do not pass sensitive values (API keys, passwords, tokens) as run-scoped variables. Instead, configure them as workspace-level sensitive variables directly in TFC. Workspace-level sensitive variables are never exposed in the UI or API responses. Run-scoped variables are appropriate for non-sensitive routing data such as tenant IDs, regions, and plan tier names.

Manual callback mode (waitForCompletion: false)

When waitForCompletion: false, the worker dispatches the TFC run and returns immediately. The ActionRun stays running until either the customer manually POSTs a callback or the reaper fires at the Action's timeout.

On dispatch, Terrantula stores the TFC run details in the ActionRun's metadata field:

{
  "runId": "run-abc123",
  "runUrl": "https://app.terraform.io/app/my-org/workspaces/tenant-onboard/runs/run-abc123",
  "workspaceId": "ws-xyz",
  "workspaceName": "tenant-onboard",
  "organization": "my-org",
  "status": "pending"
}

The callback URL and token are available in the ActionRun's interpolation context when the action is triggered ({{ run.callbackUrl }} and {{ run.callbackToken }}).

Authentication and secrets

apiToken accepts a Terrantula Secret reference ({{ secrets.tfc-api-token }}). The token must have permission to queue runs in the target workspace. TFC supports user, team, and organization tokens — any of them work.

SSRF guard

The apiBaseUrl is validated against the same SSRF blocklist as the Atlantis trigger — see the Atlantis trigger SSRF guard section for the full list of blocked ranges.

What Terrantula doesnot do

  • Does not create or manage workspaces. The workspace must exist before the action fires. Workspace lifecycle stays with the customer.
  • Does not run terraform apply. TFC runs the plan/apply; Terrantula triggers and observes.
  • Does not upload Terraform configuration. TFC pulls config from the workspace's configured VCS repo (or the workspace's current configuration version). The configSource field is informational — there is no upload mode.

Atmos workflow trigger

Invokes an Atmos workflow on a customer-deployed runner. Atmos workflows mirror Terrantula Actions architecturally — both are named, parameterized invocations against a deployment target. The trigger HTTP-POSTs a structured payload to the customer's runner endpoint; the runner clones their Atmos repo, runs atmos workflow <name> -s <stack>, and callbacks Terrantula.

trigger:
  type: atmos-workflow
  workflow: provision-tenant
  stack: "tenant-{{ parameters.customer_id }}"
  runner:
    endpoint: https://atmos-runner.example.com/dispatch
    auth:
      type: bearer
      token: "{{ secrets.atmos-runner-token }}"
  variables:
    customer_id: "{{ parameters.customer_id }}"
    plan_tier:   "{{ parameters.plan_tier }}"
    region:      "{{ recommendations.target-cluster.properties.region }}"
  waitForCompletion: true

Customers deploy a runner — either the reference terrantula-atmos-runner Docker container, or a GitHub Actions / GitLab CI workflow template — and point the trigger at it. The runner is open source and customer-deployed; Terrantula never hosts it.

The runner.endpoint is validated against the same SSRF blocklist as the Atlantis trigger — see the Atlantis trigger SSRF guard section for the full list of blocked ranges.


Atlantis trigger

Talks to a customer-hosted Atlantis instance. Two integration modes:

Pull-request mode (default) — Terrantula opens a GitHub PR with the file changes; Atlantis picks it up via its own webhook subscription and runs plan/apply. This mode is identical in behavior to the generic pull-request trigger but is explicitly labeled for Atlantis workflows and produces { mode: 'pull-request', prNumber, prUrl, repo } metadata on the ActionRun.

API-dispatch mode — Terrantula calls Atlantis's /api/plan or /api/apply endpoint directly, without a PR cycle. Returns structured project results including Terraform output. Useful for automation flows that don't require PR review.

# --- Pull-request mode ---
trigger:
  type: atlantis
  endpoint: https://atlantis.example.com    # customer-hosted; must be https://
  auth:
    type: token
    token: "{{ secrets.atlantis-token }}"            # X-Atlantis-Token header
  mode: pull-request                                  # default; omit to get this
  project: "tenant-{{ parameters.customer_id }}"
  workflow: tenant-onboard
  pullRequest:                                        # required in pull-request mode
    repo: my-org/my-terraform-repo
    auth:
      type: token
      token: "{{ secrets.github-token }}"
    title: "Onboard {{ parameters.customer_id }}"
    head: "terrantula/onboard-{{ parameters.customer_id }}"
    base: main
    files:
      - path: "tenants/{{ parameters.customer_id }}/main.tf"
        content: |
          module "tenant" {
            source      = "../../terraform"
            customer_id = "{{ parameters.customer_id }}"
          }
      - path: atlantis.yaml
        operation: patch
        patch:
          type: yaml-key
          path: "projects.0"
          value:
            name: "tenant-{{ parameters.customer_id }}"
            dir: "tenants/{{ parameters.customer_id }}"
            workflow: tenant-onboard
    webhookSecret: "{{ secrets.github-webhook-secret }}"
  variables:                                          # informational in pull-request mode
    customer_id: "{{ parameters.customer_id }}"
# --- API-dispatch mode ---
trigger:
  type: atlantis
  endpoint: https://atlantis.example.com
  auth:
    type: token
    token: "{{ secrets.atlantis-token }}"
  mode: api-dispatch
  repo: my-org/my-terraform-repo                     # required in api-dispatch mode
  ref: main                                          # required in api-dispatch mode
  vcsType: Github                                    # required; matches Atlantis config
  project: "tenant-{{ parameters.customer_id }}"
  command: plan                                      # 'plan' (default) or 'apply'
  waitForCompletion: true                            # default true

How pull-request mode works

  1. Worker resolves secrets and interpolates the trigger.
  2. Worker calls createPullRequest with the nested pullRequest config — identical to the generic pull-request trigger flow.
  3. Worker stores { mode: 'pull-request', prNumber, prUrl, repo } in the ActionRun's metadata.
  4. Atlantis picks up the PR via its existing GitHub webhook and runs plan/apply.
  5. On PR merge, Terrantula's GitHub webhook transitions the ActionRun to succeeded.

How api-dispatch mode works

  1. Worker resolves secrets and interpolates repo, ref, project.
  2. Worker POSTs to <endpoint>/api/plan or <endpoint>/api/apply with X-Atlantis-Token auth.
  3. The Atlantis API is synchronous — it blocks until the plan/apply completes.
  4. Worker parses the response, concatenates Terraform output from all ProjectResults.
  5. Worker stores { mode: 'api-dispatch', command, projectResults, planOutput } in the ActionRun's metadata.
  6. Worker POSTs to the callback URL to signal succeeded.

Authentication

Atlantis uses a custom header: X-Atlantis-Token: <api-secret>. The api-secret must be configured in Atlantis server settings (--api-secret flag or config file). The auth.token field accepts a Terrantula Secret reference ({{ secrets.atlantis-token }}).

SSRF guard

The endpoint URL is validated before any HTTP call is made. https:// is required. The shared validateInternalAddress() guard blocks: loopback (localhost, 127.0.0.0/8, ::1, 0.0.0.0), link-local (169.254.*), RFC 1918 (10.*, 172.16–31.*, 192.168.*), CGNAT (100.64.0.0/10), IPv6 ULA (fc00::/7), IPv6 multicast (ff00::/8), and IPv4-mapped IPv6 forms (::ffff:*).

Retry behavior

Retries on 5xx, 429, and network errors with exponential backoff. 4xx (except 429) fails immediately. Default: 3 attempts, 1000ms initial delay (doubles each attempt).

What Terrantula doesnot do

  • Does not host Atlantis. The customer's Atlantis instance stays where it is. Terrantula never hosts runners or Atlantis instances.
  • Does not manage Atlantis configuration. The atlantis.yaml in the customer's repo is managed by the customer (or patched via the pull-request trigger's file-patch operation).
  • Does not run terraform apply directly. In api-dispatch mode, Terrantula asks Atlantis to run apply; Atlantis runs it on its own infrastructure.

Per-environment trigger overrides

A single Action can override trigger fields per environment via envOverrides. Useful when the same Action targets different webhook URLs / TFC workspaces / Atlantis instances / GitHub repos in dev vs prod.

trigger:
  type: webhook
  url: https://workflows-prod.example.com/onboard
  # ... rest of base trigger
envOverrides:
  staging:
    type: webhook
    url: https://workflows-staging.example.com/onboard
  dev:
    type: webhook
    url: https://workflows-dev.example.com/onboard

At dispatch time, override fields are shallow-merged on top of the base trigger. The type must match the base trigger's type — enforced at apply.