Interpolation reference

Action triggers support {{ ... }} interpolation in payloads, headers, auth values, URLs, PR titles, PR bodies, file contents, branch names, and labels. The same syntax works in every interpolation-aware field.

Available variables

VariableAvailable when
{{ secrets.<name> }}Always — resolves and decrypts the named Terrantula Secret
{{ parameters.<name> }}Always — values supplied when the Action was triggered
{{ recommendations.<name>.id }}Action has recommendations
{{ recommendations.<name>.name }}Action has recommendations
{{ recommendations.<name>.properties.<prop> }}Action has recommendations
{{ entity.id }}Action scope: instance
{{ entity.name }}Action scope: instance
{{ entity.properties.<prop> }}Action scope: instance
{{ entity.labels.<label> }}Action scope: instance
{{ run.id }}Always — the ActionRun UUID
{{ run.callbackUrl }}Always — where the external workflow POSTs completion
{{ run.callbackToken }}Always — HMAC-signed token for that callback (see below)
{{ operation.entity.id }}Inside createRelationship on a create-entity operation — refers to the just-created entity

Callback token security

{{ run.callbackToken }} is HMAC-SHA256 signed using the server encryption key. The token is bound to:

  • The organization that owns the run
  • The specific ActionRun UUID
  • The expiry timestamp (defaults to the Action's timeout, or 60 minutes if unset)

Format: {orgId}:{runId}:{expiresAtMs}:{hmac}. The callback endpoint validates with constant-time comparison and a status guard — a token issued for one run cannot be replayed against another run, even within the expiry window. The organization is inside the signature and is the tenant the callback is served in, so a token cannot reach a run belonging to anyone else.

This token is not a credential by itself. It can be embedded in PR bodies, log messages, or anywhere safe to display — the HMAC binds it to a specific run and expiry, and the status guard prevents replay.

How a workflow presents it

The callback takes two credentials, and they travel in two different headers:

HeaderCarries
Authorization: Bearer <project API token>a project principal holding at least data:read. This is a floor: it proves the caller is a known principal in the project, and nothing more.
X-Terrantula-Callback-Token: {{ run.callbackToken }}the per-run HMAC. This is what actually authorizes completing this run.

A request carrying only one of the two is refused with 401. Issue the project API token from Project settings → Tokens with the read role and store it alongside your workflow's other secrets; it is long-lived, unlike the callback token.

curl --fail --silent --show-error \
  -X POST "$CALLBACK_URL" \
  -H "Authorization: Bearer $TERRANTULA_API_TOKEN" \
  -H "X-Terrantula-Callback-Token: $CALLBACK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status": "succeeded"}'

Run ID safety

{{ run.id }} (the bare UUID without HMAC) is always safe to embed in PR bodies, commit messages, or logs. It identifies a run for human reference but cannot authorize a callback on its own.

Examples

Webhook payload

trigger:
  type: webhook
  url: https://workflows.example.com/onboard
  auth:
    type: bearer
    token: "{{ secrets.onboarding-webhook-token }}"
  payload:
    customerId: "{{ entity.id }}"
    customerName: "{{ entity.properties.companyName }}"
    targetCluster: "{{ recommendations.targetCluster.id }}"
    namespace: "{{ parameters.namespace }}"
    callbackUrl: "{{ run.callbackUrl }}"
    callbackToken: "{{ run.callbackToken }}"

Pull-request trigger

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 }}`.

    Tenant: {{ entity.properties.companyName }}
    Target cluster: {{ recommendations.targetCluster.name }}
    Region: {{ recommendations.targetCluster.properties.region }}
  head: "terrantula/onboard-{{ entity.id }}"
  base: main
  reviewers:
    - "{{ parameters.approver }}"
  files:
    - path: "tenants/{{ entity.id }}.yaml"
      content: |
        apiVersion: v1
        kind: Tenant
        metadata:
          name: "{{ entity.properties.companyName }}"
          cluster: "{{ recommendations.targetCluster.id }}"
  webhookSecret: "{{ secrets.github-webhook-secret }}"

Action withcreateRelationship

operation:
  type: create-entity
  entityType: Tenant
  onTrigger: provisioning
  onSuccess: active
  onFailure: failed
  properties:
    customer_id: "{{ parameters.customer_id }}"
  createRelationship:
    relationshipType: runs_on
    to: "{{ recommendations.target-cluster.id }}"
    onSuccess: active
    onFailure: removing
    properties:
      namespace: "tenant-{{ operation.entity.id }}"  # The just-created Tenant's ID

In createRelationship, the special variable {{ operation.entity.id }} resolves to the entity that was just created by the parent create-entity operation. This lets you compose entity creation and relationship creation atomically without two separate calls.