Skip to content

[SPEC] terramate list --format json — Machine-readable structured output #2324

@mariux

Description

@mariux

SPEC: terramate list --format json

Machine-readable structured output for terramate list.

Background

Users need to consume stack metadata programmatically — for CI/CD workflow matrices, external orchestration config generation (e.g. Atlantis), and automation scripts. The current text output requires fragile shell parsing and cannot represent structured data like dependencies, bundle provenance, or sharing relationships.

CLI Interface

terramate list --format json [--include-host-dir] [--include-change-status] [--include-cloud-status] [existing flags...]
  • --format json — outputs JSON array to stdout. Extensible to other formats later.
  • --include-host-dir — opt-in: adds host object (filesystem paths) to all path objects.
  • --include-change-status — opt-in: runs change detection and adds status.changed and status.change_reasons to every stack. Does not filter stacks — all stacks are still returned. Implicitly enabled when --changed is used.
  • --include-cloud-status — opt-in: queries Terramate Cloud and adds status.cloud to every stack.
  • Compatible with all existing list flags: --changed, --tags, -C, --run-order, --target, --why, cloud filters, etc. --why is redundant with --format json since change_reasons provides the same information — the flag is accepted but has no additional effect on JSON output.
  • JSON on stdout, human messages on stderr.
  • Existing terramate list behavior is unchanged when --format is not specified.

Status block behavior

The status object is absent when no status flags are active. Its fields appear based on flags:

Flags Stacks returned status.changed status.change_reasons status.cloud
--format json all absent absent absent
--format json --changed only changed true (always) present absent
--format json --include-change-status all true or false present when true, absent when false absent
--format json --include-cloud-status all absent absent present
--format json --changed --include-cloud-status only changed true (always) present present

Note: --changed implicitly enables --include-change-status.

Top-Level Structure

JSON array of stack objects:

[
  { ... stack object ... },
  { ... stack object ... }
]

When no stacks match the filters, the output is an empty array [].

Stack Object

Note: The example below assumes --include-change-status --include-cloud-status flags are active. Without those flags, the status object is absent.

{
  "id": "a1111111-1111-1111-1111-111111111111",
  "name": "vpc",
  "description": "VPC networking stack",
  "path": {
    "absolute": "/stacks/networking/vpc"
  },
  "tags": ["aws", "core", "network"],
  "watch": [
    {
      "path": {
        "absolute": "/config/networking.cfg",
        "relative": "../../../config/networking.cfg"
      },
      "exists": true
    }
  ],
  "is_nested": true,
  "parent": {
    "id": "p1111111-1111-1111-1111-111111111111",
    "path": {
      "absolute": "/stacks/networking",
      "relative": ".."
    }
  },
  "raw": {
    "id": "a1111111-1111-1111-1111-111111111111",
    "name": "vpc",
    "description": "VPC networking stack",
    "tags": ["core"],
    "before": [],
    "after": [],
    "wants": [],
    "wanted_by": [],
    "watch": ["../../../config/networking.cfg"]
  },
  "before": {
    "refs": [],
    "stacks": []
  },
  "after": {
    "refs": [
      {
        "ref": "tag:hub",
        "type": "tag",
        "stacks": [
          {
            "id": "h1111111-1111-1111-1111-111111111111",
            "path": {
              "absolute": "/stacks/networking/hub",
              "relative": "../hub"
            }
          }
        ]
      },
      {
        "ref": "../dns",
        "type": "path",
        "stacks": [
          {
            "id": "d1111111-1111-1111-1111-111111111111",
            "path": {
              "absolute": "/stacks/networking/dns",
              "relative": "../dns"
            }
          }
        ]
      }
    ],
    "stacks": [
      {
        "id": "h1111111-1111-1111-1111-111111111111",
        "path": {
          "absolute": "/stacks/networking/hub",
          "relative": "../hub"
        }
      },
      {
        "id": "d1111111-1111-1111-1111-111111111111",
        "path": {
          "absolute": "/stacks/networking/dns",
          "relative": "../dns"
        }
      }
    ]
  },
  "wants": {
    "refs": [],
    "stacks": []
  },
  "wanted_by": {
    "refs": [],
    "stacks": []
  },
  "sharing": {
    "outputs": [
      {
        "name": "vpc_id",
        "backend": "terraform"
      }
    ],
    "inputs": [
      {
        "name": "hub_vpc_id",
        "backend": "terraform",
        "from_stack": {
          "id": "h1111111-1111-1111-1111-111111111111",
          "path": {
            "absolute": "/stacks/networking/hub",
            "relative": "../hub"
          }
        }
      }
    ]
  },
  "status": {
    "changed": true,
    "change_reasons": [
      { "message": "stack has been triggered by: /stacks/networking/vpc/.tmtrigger" }
    ],
    "cloud": {
      "stack_status": "ok",
      "deployment_status": "ok",
      "drift_status": "ok"
    }
  },
  "bundles": [
    {
      "instance": {
        "name": "my-vpc",
        "uuid": "b1111111-1111-1111-1111-111111111111",
        "alias": "vpc-main",
        "source": "example.com/tf-aws-vpc/v1",
        "environment": "production"
      },
      "definition": {
        "class": "infrastructure",
        "name": "AWS VPC",
        "version": "1.0.0",
        "description": "Provisions a VPC with subnets"
      },
      "metadata": {
        "name": "vpc",
        "description": "VPC networking stack",
        "tags": ["aws", "network"],
        "before": [],
        "after": ["tag:hub", "../dns"],
        "wants": [],
        "wanted_by": [],
        "watch": ["../../../config/networking.cfg"]
      }
    }
  ],
  "components": [
    {
      "instance": {
        "name": "vpc",
        "source": "./components/vpc",
        "resolved_source": "/components/example.com/tf-aws-vpc/v1"
      },
      "definition": {
        "class": "terraform",
        "name": "AWS VPC Component",
        "version": "1.0.0",
        "description": "Terraform VPC module"
      },
      "bundle": {
        "class": "infrastructure",
        "alias": "vpc-main",
        "uuid": "b1111111-1111-1111-1111-111111111111"
      }
    },
    {
      "instance": {
        "name": "monitoring",
        "source": "./components/monitoring",
        "resolved_source": "/components/monitoring/v1"
      },
      "definition": {
        "class": "observability",
        "name": "Monitoring",
        "version": "2.0.0",
        "description": "Monitoring setup"
      },
      "bundle": null
    }
  ]
}

Path Object

The path object appears in multiple places with context-dependent fields.

Top-level stack path (default)

"path": {
  "absolute": "/stacks/vpc"
}

Top-level stack path (with --include-host-dir)

"path": {
  "absolute": "/stacks/vpc",
  "host": {
    "abs": "/home/user/repo/stacks/vpc",
    "rel": "stacks/vpc"
  }
}

Nested reference path (dependency, parent, sharing, watch)

"path": {
  "absolute": "/stacks/hub",
  "relative": "../../hub"
}

relative means relative to the owning/referencing stack's directory. For dependency refs this is the stack that declares the before/after/wants/wanted_by. For watch paths this is the stack that declares the watch. This matches how these references are written in HCL.

Nested reference path (with --include-host-dir)

"path": {
  "absolute": "/stacks/hub",
  "relative": "../../hub",
  "host": {
    "abs": "/home/user/repo/stacks/hub",
    "rel": "../hub"
  }
}

Field definitions

Field Meaning Present
absolute Project-absolute path (from repo root, starts with /) Always
relative Relative to the owning/referencing stack's directory Nested refs only (deps, parent, sharing, watch)
host.abs Filesystem absolute path --include-host-dir only
host.rel Filesystem relative to CWD or -C chdir --include-host-dir only

Field Reference

Stack fields (always present)

Field Type Description
id string|null Stack UUID. null when no ID is set in the stack definition.
name string Stack name (merged result)
description string Stack description (merged result)
path object Path object (see above)
tags string[] Tags, sorted alphanumerically (merged result)
watch object[] Watch files with resolved paths and existence flag (merged result)
is_nested bool true if stack has a parent stack
parent object|null Parent stack reference (id + path), or null
raw object Stack's own values from stack {} block before bundle merging
before object Before dependencies (merged result, see dependency object)
after object After dependencies (merged result, see dependency object)
wants object Wants dependencies (merged result, see dependency object)
wanted_by object Wanted-by dependencies (merged result, see dependency object)
sharing object Output sharing info (see sharing object). Absent when stack has no sharing config.
status object Status information. Absent when no --include-*-status or --changed flags are used. See status object.
bundles array Bundles managing this stack. Absent when stack has no bundles.
components array Active components in this stack. Absent when stack has no active components.

Top-level scalar fields (name, description) and list fields (tags, watch) show the merged result after all bundle contributions. The raw object and bundles[].metadata allow tracing each value to its origin.

Naming note: At the stack level, pre-merge values are called raw because they represent the literal HCL config from the stack {} block. At the bundle and component level, the equivalent is called definition because it maps to the define bundle / define component blocks in the source.

Raw object

The stack's own values from the stack {} block, exactly as the user configured them (before normalization or bundle merging):

{
  "id": "a1111111-1111-1111-1111-111111111111",
  "name": "vpc",
  "description": "VPC networking stack",
  "tags": ["core"],
  "before": [],
  "after": [],
  "wants": [],
  "wanted_by": [],
  "watch": ["../../../config/networking.cfg"]
}

All values are the literal HCL config values. Watch paths are the raw strings as written (relative or absolute), not resolved.

  • id — present only when explicitly set in the stack {} block, omitted otherwise
  • Other fields are always present (empty string or empty array when not configured)

Merge rules:

  • name, description: stack definition wins; bundle value used only if stack's own value is empty
  • tags, before, after, wants, wanted_by, watch: union of stack + all bundle contributions, deduplicated, sorted alphanumerically

Dependency object (before, after, wants, wanted_by)

{
  "refs": [
    {
      "ref": "<raw reference as written in stack definition>",
      "type": "tag|path",
      "stacks": [
        { "id": "...", "path": { "absolute": "...", "relative": "..." } }
      ]
    }
  ],
  "stacks": [
    { "id": "...", "path": { "absolute": "...", "relative": "..." } }
  ]
}
  • refs — per-reference detail: raw ref string, type ("tag" for tag: filter expressions, "path" for relative/absolute paths), and resolved stacks (one ref can resolve to multiple stacks via tags or directory matching). When a ref resolves to nothing (e.g. deleted stack, no matching tags), stacks is an empty array.
  • stacks — flat deduplicated list of all resolved stacks across all refs

Sharing object

Absent when the stack has no input or output blocks configured.

{
  "outputs": [
    { "name": "...", "backend": "..." }
  ],
  "inputs": [
    {
      "name": "...",
      "backend": "...",
      "from_stack": { "id": "...", "path": { "absolute": "...", "relative": "..." } }
    }
  ]
}
  • No value expressions — only structural references.
  • from_stack uses the same stack reference pattern (id + path).

Status object

Absent when no status flags are active. Contains fields based on flags.

Change detection fields (with --include-change-status or --changed)

Changed stack:

{
  "changed": true,
  "change_reasons": [
    { "message": "stack has been triggered by: /stacks/networking/vpc/.tmtrigger" }
  ]
}

Unchanged stack (only with --include-change-status, never with --changed alone since those are filtered out):

{
  "changed": false
}
  • changed — bool, always present when change detection is active
  • change_reasons — absent when changed is false. Must be an array of objects even if the current implementation only returns one reason per stack — this is a forward-compatibility requirement to support multiple reasons and additional fields (e.g. type, file, details) in the future without a breaking schema change.

status.cloud (with --include-cloud-status)

Stack synced to Cloud:

{
  "stack_status": "ok",
  "deployment_status": "ok",
  "drift_status": "ok"
}

Stack is a draft:

{
  "stack_status": "ok",
  "deployment_status": "ok",
  "drift_status": "ok",
  "draft": true
}

Stack not in Cloud:

{
  "missing": true
}
  • missing — absent when false, only present when true
  • draft — absent when false, only present when true
  • stack_status, deployment_status, drift_status — absent when missing is true

Status values per field:

Field Values
stack_status ok, drifted, failed, unrecognized
deployment_status ok, pending, running, failed, canceled, unrecognized
drift_status ok, unknown, drifted, failed, running, unrecognized

Bundle object

{
  "instance": {
    "name": "...",
    "uuid": "...",
    "alias": "...",
    "source": "...",
    "environment": "..."
  },
  "definition": {
    "class": "...",
    "name": "...",
    "version": "...",
    "description": "..."
  },
  "metadata": {
    "name": "...",
    "description": "...",
    "tags": [],
    "before": [],
    "after": [],
    "wants": [],
    "wanted_by": [],
    "watch": []
  }
}
  • instance — the bundle instance (from bundle "name" {} block)
  • definition — the bundle definition metadata (from define bundle { metadata {} })
  • metadata — the fields/values the bundle contributes to this stack (duplicates of top-level fields showing bundle's contribution)

Component object

{
  "instance": {
    "name": "...",
    "source": "...",
    "resolved_source": "..."
  },
  "definition": {
    "class": "...",
    "name": "...",
    "version": "...",
    "description": "..."
  },
  "bundle": {
    "class": "...",
    "alias": "...",
    "uuid": "..."
  }
}
  • instance — the component instance
  • definition — the component definition metadata (from define component { metadata {} })
  • bundle — reference to originating bundle (null if standalone component)
  • Only active (non-skipped) components are listed

Scope

In scope

Out of scope

Related Issues & PRs

Type # Title Disposition
ISSUE #723 Json stack list output Partially covered (modules aspect remains open)
ISSUE #2069 list --run-order to include specific ordering Partially covered (JSON output covered, run-order aspect remains open)
ISSUE #1960 Command to get stack ID Closed — covered by JSON list + eval
ISSUE #1353 Run against a stack by name or id Partially covered (CI matrix covered, run --id remains open)
PR #2073 Add JSON format to list command Closed — superseded
PR #2219 list --format {text,json,dot} --label Closed — superseded

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions