Action

An Action is the only way infrastructure changes in Terrantula. It pairs a graph operation (create/update/transition an entity or relationship) with an external trigger that does the real work — opening a pull request, firing a Terraform Cloud run, dispatching an Atmos workflow, or calling Atlantis.

INFO

Terrantula never runs terraform apply itself. Actions open pull requests against your repo or dispatch runs to your runner; your CI applies. The graph is a read-only projection of the resulting TF-derived state.

Phase: schema. This is the largest kind — operations, triggers, recommendations, cascade rules, and per-env overrides are all defined here.

Minimal example

kind: Action
name: SuspendTenant
associatedWith:
  entityType: Tenant
  scope: instance
operation:
  type: transition-entity
  onTrigger: suspending
  onSuccess: suspended
  onFailure: active
trigger:
  type: noop

Fields

FieldTypeRequiredDescription
kind"Action"yesDiscriminator. Always Action.
namestringyesUnique action name within the project. Referenced by applier, dependsOn, and triggers.
displayNamestringnoHuman label for the UI.
descriptionstringnoHelp text shown in the Action picker.
associatedWithAssociatedWithyesWhich EntityType the Action attaches to, and whether it appears at the collection or instance level.
conditionsCondition[]noAll must pass for the Action to be available on an entity. Default [].
parametersProperty[]noInputs collected when the Action is triggered. Default []. Same shape as EntityType properties.
recommendationsRecommendation[]noCell-ranked placement suggestions surfaced at trigger time. Default [].
operationOperationyesThe graph mutation. One of eight operation types.
triggerTriggeryesThe external dispatch. One of six trigger types.
envOverridesobject (env→TriggerOverride)noPer-env partial overrides shallow-merged onto the base trigger.
dependsOnDependsOn[]noOther Apply Actions that must have succeeded for related entities first.
timeoutpositive integernoMinutes an ActionRun may be in-flight before the reaper marks it failed. Default 60.
mutatesPropertystringnoNames the entity property this Action changes. Drives the graph Action picker. Must reference a declared property of associatedWith.entityType.

AssociatedWith

FieldTypeRequiredDescription
entityTypestringyesName of the EntityType the Action attaches to.
scope"collection" | "instance"yescollection: appears at the type level (create new). instance: appears on each existing entity (modify it).

Condition

All conditions must pass for the Action to be offered on an entity (evaluated against the entity for instance-scoped actions).

FieldTypeRequiredDescription
fieldstringyesDot-path to the value being tested. Valid prefixes: entity.state, entity.properties.*, entity.metrics.*, entity.labels.*.
operatoreq | neq | in | gt | lt | gte | lteyesComparison.
valuestring | number | boolean | string[]yesRight-hand operand (use a string array with in).

Recommendation

A cell-ranked placement suggestion. At trigger time Terrantula ranks the cell's members and proposes the best target; the selection is then available to the operation and trigger via {{ recommendations.<name>.* }}.

FieldTypeRequiredDescription
namestringyesRecommendation key (used in interpolation).
titlestringyesHuman label.
cellstringyesName of the Cell whose members are ranked. Must reference an existing Cell.
sortBystringyesMetric name to rank members by.
order"asc" | "desc"noSort direction. Default asc.
requiredbooleannoWhether a selection is mandatory. Default true.

Operations

operation is discriminated on type. Most carry onTrigger / onSuccess / onFailure states that drive the entity (or relationship) through its lifecycle as the ActionRun progresses.

typeWhat it doesKey fields
create-entityCreates a new entity (collection scope).entityType, optional name, optional addToCell, onTrigger/onSuccess/onFailure, optional properties, optional createRelationship, optional cascadeRules. onFailure may be "delete".
delete-entityDeletes the entity and its relationships on success.onTrigger, onSuccess: "delete", onFailure.
update-entityUpdates properties and/or state.onTrigger/onSuccess/onFailure, optional properties, optional state.
transition-entityTransitions entity state with optional cascade.onTrigger/onSuccess/onFailure, optional cascadeRules.
create-relationshipCreates an edge between two entities.relationshipType, from, to, onTrigger/onSuccess/onFailure, optional properties. onFailure may be "delete".
delete-relationshipDeletes an edge on success.relationshipType, onTrigger, onSuccess: "delete", onFailure.
update-relationshipUpdates edge properties.relationshipType, onTrigger/onSuccess/onFailure, optional properties.
migrate-relationshipRe-points an edge to a new to-entity (rebalance). Bypasses property-sum constraints.relationshipType, to, onTrigger/onSuccess/onFailure.

createRelationship

Inside a create-entity operation, optionally create a relationship from the just-created entity on success:

FieldTypeRequiredDescription
relationshipTypestringyesRelationshipType name.
tostringyesInterpolation expression for the to-entity ID (typically {{ recommendations.<name>.id }}).
onSuccessstringyesState for the new relationship on success.
onFailurestringyesState for the new relationship on failure.
propertiesobject (string→string)noProperty values; supports interpolation.

Triggers

trigger is discriminated on type. Six types, covering the substrates Terrantula sits on top of — never replacing your runners.

typeIntegrationNotes
pull-requestGitHub PROpens a PR, optionally fires repository_dispatch on merge. Auto-completes on merge via webhook.
terraform-cloudTFC / HCP TerraformFires a run against a pre-existing workspace. Terrantula never creates workspaces.
atlantisAtlantisPR mode (default) or direct api-dispatch. Atlantis is always customer-hosted.
atmos-workflowAtmosInvokes a named workflow on a customer-deployed reference runner.
webhookGeneric HTTPCalls an arbitrary URL with an interpolated payload and retry policy.
noopnoneTransitions the run to succeeded immediately; for Actions whose whole effect is in-Terrantula (e.g. cascade-only).

pull-request

FieldTypeRequiredDescription
type"pull-request"yes
repostringyesowner/repo. Supports interpolation.
auth{ type: "token", token?: string }yestoken is optional: when omitted, the runtime resolves an installation token via the linked GitHub App.
titlestringyesPR title. Supports interpolation.
bodystringnoPR body. {{ run.id }} is safe to embed as a reference.
headstringyesBranch to create. Supports interpolation.
basestringnoBase branch. Default main.
filesFileEntry[] (min 1)yesFiles to commit.
labelsstring[]noPR labels.
reviewersstring[]noGitHub usernames to request review from.
teamReviewersstring[]noGitHub team slugs to request review from.
webhookSecretstringnoSecret holding the repo's webhook HMAC secret. Required for auto-completion on merge; omit if you complete runs manually.
postMergeDispatchPostMergeDispatchnoAfter merge, fire a repository_dispatch and wait for the workflow callback.

FileEntry

FieldTypeRequiredDescription
pathstringyesPath relative to repo root. Supports interpolation.
operation"replace" | "patch"noreplace (default) commits content. patch fetches the file and applies patch.
contentstringconditionalRequired when operation: replace. Supports interpolation.
patchFilePatchconditionalRequired when operation: patch.

FilePatch

Patch mode lets a cattle workflow extend a monolithic tfvars/yaml file without refactoring to one-file-per-tenant. Discriminated on type:

typeFieldsBehavior
json-mergepointer (RFC 6901, default ""), valueRFC 7396 JSON Merge Patch at the pointer; null fields delete keys.
json-array-appendpointer, valueAppend value to the JSON array at the pointer.
yaml-keypath (dot-separated; integer segments are array indices), valueSet a key in a YAML document, preserving comments and formatting.

postMergeDispatch

FieldTypeRequiredDescription
workflowstringyesWorkflow filename (informational).
eventTypestringyesrepository_dispatch event_type.
repostringnoOverride the target repo for the dispatch. Defaults to the trigger's repo.
payloadobject (string→string)yesclient_payload. Supports interpolation; the runtime auto-injects callback_url and callback_token.

terraform-cloud

Fires a run against a pre-existing workspace. Terrantula tracks the outcome; TFC runs the plan/apply.

FieldTypeRequiredDescription
type"terraform-cloud"yes
organizationstringyesTFC organization. Supports interpolation.
workspaceNamestringconditionalWorkspace name. Exactly one of workspaceName / workspaceId.
workspaceIdstringconditionalWorkspace ID (ws-xxxxxxxx). Exactly one of the two.
apiTokenstringyesSecret holding the TFC API token, e.g. {{ secrets.tfc-api-token }}.
apiBaseUrlstring (https)noTFC API base URL. Default https://app.terraform.io. Must be https.
configSource{ type: "vcs", repo?, branch? } or { type: "current" }noWhere TFC pulls config from. Defaults to the workspace's current config.
variablesobject (string→string)noRun-scoped TF variables. Interpolated, then JSON-stringified as HCL literals.
autoApplybooleannoAuto-apply after a successful plan. Default false (plan-only).
waitForCompletionbooleannoPoll until terminal. Default true.
pollIntervalSecondsint 2–60noPoll interval. Default 10.
messagestringnoMessage attached to the TFC run. Supports interpolation.

atlantis

Atlantis is always customer-hosted. Two modes:

FieldTypeRequiredDescription
type"atlantis"yes
endpointstring (https)yesCustomer-hosted Atlantis base URL. Must be https.
auth{ type: "token", token }yesX-Atlantis-Token value, e.g. {{ secrets.atlantis-token }}.
mode"pull-request" | "api-dispatch"noDefault pull-request.
pullRequestPR configconditionalRequired when mode: pull-request. Same fields as the pull-request trigger.
repo / ref / vcsTypestringconditionalRequired when mode: api-dispatch.
projectstringnoAtlantis project name. Supports interpolation.
workflowstringnoAtlantis workflow name (informational).
command"plan" | "apply"noapi-dispatch only. Default plan.
waitForCompletionbooleannoDefault true.
pollIntervalSecondsint 2–60noDefault 10.

atmos-workflow

Invokes a named Atmos workflow on a customer-deployed reference runner. Terrantula never executes atmos directly.

FieldTypeRequiredDescription
type"atmos-workflow"yes
workflowstringyesAtmos workflow name. Supports interpolation.
stackstringyesAtmos stack identifier, e.g. tenant-{{ parameters.customer_id }}.
runner.endpointstring (https)yesRunner HTTP endpoint. Must be https.
runner.authbearer or header authnoOnly bearer/header are accepted (the reference runner's tested auth types).
variablesobject (string→string)noVariables passed to the workflow. Supports interpolation.
waitForCompletionbooleannoDefault true.

Cascade rules

cascadeRules (on create-entity and transition-entity operations) propagate changes along the graph when the parent entity enters a phase. Phases: on-trigger, on-success, on-failure. Two flavors, discriminated by which payload is present:

State-transition rule — move existing relationships into a new state:

FieldTypeRequiredDescription
phaseon-trigger | on-success | on-failureyesWhen the rule fires.
relationshipTypestringyesRelationshipType to transition.
fromStatestringyesOnly edges in this state are eligible.
toStatestringyesTarget state.
directionfrom | to | bothnoWhich end the transitioning entity occupies. Default both.

createEntity rule — seed a sibling entity + a linking relationship:

FieldTypeRequiredDescription
phaseon-trigger | on-success | on-failureyesWhen the rule fires.
createEntity.typestringyesEntityType of the created sibling.
createEntity.namestringyesName template. Supports interpolation.
createEntity.propertiesobjectnoProperty values; supports interpolation. Default {}.
createEntity.relationship{ type, direction: "from" | "to" }yesRelationship linking parent to the new entity.

dependsOn

Sequences Apply Actions across the entity relationship graph — the high-level golden path. Both fields are literal name references (no interpolation), validated at apply.

FieldTypeRequiredDescription
relationshipstringyesRelationshipType name linking this entity to its upstream.
appliedActionstringyesAction name that must have succeeded for the linked upstream entity first.

Apply-time validation: DEPENDS_ON_RELATIONSHIP_NOT_FOUND, DEPENDS_ON_ACTION_NOT_FOUND, and DEPENDS_ON_CYCLE (the dependency graph, including edges from prior applies, must be acyclic).

Per-env overrides

envOverrides is a map of env name → a partial of the base trigger (same type, validated at apply). At dispatch, override fields are shallow-merged over the base trigger. Use it to point dev/staging/prod at different repos, workspaces, or runner endpoints from one Action definition.

Annotated example: create-entity + TFC

OnboardTenant from the SaaS-tenants demo — creates a Tenant, links it to the least-loaded cluster, and fires a TFC run:

apiVersion: terrantula.io/v1
kind: Action
metadata:
  name: OnboardTenant
spec:
  displayName: Onboard a new tenant
  associatedWith:
    entityType: Tenant
    scope: collection
  parameters:
    - { name: customer_id,       type: string, required: true }
    - { name: plan_tier,         type: string, enum: [basic, premium], required: true }
    - { name: region_preference, type: string, required: true }
  recommendations:
    - name: target-cluster
      title: Target cluster
      cell: prod-clusters
      sortBy: tenant-count
      order: asc
      required: true
  operation:
    type: create-entity
    entityType: Tenant
    onTrigger: provisioning
    onSuccess: active
    onFailure: failed
    properties:
      customer_id: "{{ parameters.customer_id }}"
      plan_tier: "{{ parameters.plan_tier }}"
    createRelationship:
      relationshipType: runs_on
      to: "{{ recommendations.target-cluster.id }}"
      onSuccess: active
      onFailure: removing
      properties:
        namespace: "tenant-{{ parameters.customer_id }}"
  trigger:
    type: terraform-cloud
    organization: my-org
    workspaceName: tenant-onboard
    apiToken: "{{ secrets.tfc-api-token }}"
    variables:
      customer_id: "{{ parameters.customer_id }}"
      region: "{{ recommendations.target-cluster.properties.region }}"
    autoApply: false
    waitForCompletion: true

Annotated example: pull-request + dependsOn + postMergeDispatch

ApplyTenant from the multi-stack greenfield demo — opens a PR once all upstream applies have succeeded, then dispatches the apply workflow on merge:

kind: Action
name: ApplyTenant
associatedWith:
  entityType: Tenant
  scope: instance
dependsOn:
  - relationship: runs_in_aws_account
    appliedAction: ApplyAWSAccount
  - relationship: placed_on_cluster
    appliedAction: ApplyClusterBinding
operation:
  type: transition-entity
  onTrigger: applying
  onSuccess: active
  onFailure: failed
trigger:
  type: pull-request
  repo: "{{ project.iac_repo }}"
  auth:
    type: token
    token: "{{ secrets.github-token }}"
  title: "Apply Tenant {{ entity.name }}"
  head: "terrantula/apply-tenant-{{ entity.name }}"
  base: main
  files:
    - path: "infra/tenant/{{ entity.name }}/main.tf"
      content: |
        module "tenant" {
          source      = "../_modules/tenant"
          customer_id = "{{ entity.properties.customer_id }}"
        }
  webhookSecret: "{{ secrets.github-webhook-secret }}"
  postMergeDispatch:
    workflow: terrantula-entity-apply.yml
    eventType: terrantula-entity-apply
    payload:
      entity_id: "{{ entity.id }}"
      directory: "infra/tenant/{{ entity.name }}"

Annotated example: transition-entity + cascade

DeprovisionTenant from the SaaS-tenants demo — transitions the tenant and cascades its runs_on edge to removing then removed:

kind: Action
name: DeprovisionTenant
associatedWith:
  entityType: Tenant
  scope: instance
conditions:
  - field: entity.state
    operator: eq
    value: active
operation:
  type: transition-entity
  onTrigger: deprovisioning
  onSuccess: decommissioned
  onFailure: active
  cascadeRules:
    - relationshipType: runs_on
      fromState: active
      toState: removing
      phase: on-trigger
      direction: from
    - relationshipType: runs_on
      fromState: removing
      toState: removed
      phase: on-success
      direction: from
trigger:
  type: terraform-cloud
  organization: my-org
  workspaceName: tenant-deprovision
  apiToken: "{{ secrets.tfc-api-token }}"
  variables:
    destroy_mode: "true"
  waitForCompletion: true

Caveats

INFO

associatedWith.entityType, recommendations[].cell, every operation's relationshipType / entityType, and cascadeRulesreferences are all validated at apply against the schema kinds — declare them earlier in the same blueprint or in a prior apply.

WARNING

Auto-completion of a pull-request run on merge requires a webhook configured on the repo (webhookSecret) and GITHUB_WEBHOOK_SECRET set on the server. Without it, the run stays running until an external caller posts to the callback URL — {{ run.id }}is safe to embed in the PR body as a reference identifier.

WARNING

migrate-relationship bypasses the property-sumconstraint on the RelationshipType — intentionally, so a rebalance is not blocked by the ceiling it relieves.

NOTE

noop triggers transition the run to succeeded as soon as the operation commits, with no external HTTP call — use them for Actions whose entire effect is in-Terrantula (e.g. a create-entity that only seeds a graph via cascadeRules).