Compatibility · Terraform 1.14 · OpenTofu 1.12 · Turf 0.16
Turf is built on the OpenTofu framework — the whole expression language, the function library, and the provider ecosystem. It runs ahead of OpenTofu on actions and deferred changes, and it publishes every gap. Where a construct isn’t supported yet, Turf refuses loudly — it never silently mis-plans.
| Construct | Terraform 1.14 | OpenTofu 1.12 | Turf 0.16 | Notes |
|---|---|---|---|---|
resource | ✅ | ✅ | ✅ | Full resource support; also on the plot authoring whitelist. |
data | ✅ | ✅ | ✅ | Data sources, declarable in a plot as well as codified. Each is read once per phase through the walk, and the value the plan read is the value the apply commits to state — so what you approved is what gets stored. A read is dropped from state when its declaration leaves the configuration. |
module | ✅ | ✅ | ✅ | Recursive registry / local / git module install and expansion. The call is held to the child module’s own declarations before the walk begins — a missing required input and an undeclared argument are both refused up front, every offending call and input in one list, so a typo appears next to the input it failed to set. A module source may reference var values, resolved from the same root variables tofu init would use. |
variable | ✅ | ✅ | ✅ | Fully honored. The declared surface (type / default / sensitive) is modeled, and values resolve through the full Terraform ladder: declaration defaults, then TF_VAR_ environment variables, then terraform.tfvars and *.auto.tfvars(.json) in lexical order, then session-supplied values, then -var-file in the order listed, then -var. Types convert, nullable applies, and validation {} blocks are evaluated — a malformed value fails only itself, attributed to the source it came from. |
output | ✅ | ✅ | ✅ | Outputs, including sensitive ones — a sensitive-valued output must be declared sensitive (a loud error, never silent auto-inference). |
locals | ✅ | ✅ | ✅ | Fully honored in the codified walk. Authoring an ad-hoc plot local isn’t wired yet — an additive convenience, not a language gap (author in codified form or promote). |
provider | ✅ | ✅ | ✅ | The configuration is the only source of provider configuration: the walk evaluates each provider {} block in dependency position on every plan and configures the provider right there. Secrets stay out of the configuration as var references, supplied at plan time and held in memory only. Provider arguments resolve through locals, variables, and module outputs in dependency order, and a provider that reads a resource it serves is reported as a cycle at plan time rather than quietly mis-ordered. |
terraform {} settings | ✅ | ✅ | ✅ | Hand-authored and read by the walk. In a plot, backend / required_providers / provider_meta are folded from per-unit metadata. |
terraform.required_version | ✅ | ✅ | ⚠️➖ | Parsed but not yet enforced — the core version constraint isn’t checked today. Coming soon. (Provider constraints in required_providers are a separate matter: those are honored at resolution and enforced at plan time.) |
backend | ✅ | ✅ | ✅ | State backends. The declared backend {} block is what a workspace opens; the caller layers only the partial configuration the block deliberately leaves out, merged per key — the tofu init -backend-config pattern — and the body may reference var values. With no declaration the default is local, as upstream. OpenTofu adds state / plan encryption; Turf supports it through the backend (end-to-end verification coming). |
provisioner / connection | ✅ | ✅ | ⛔/⚠️ | Refused as config keys — there is no provisioner runtime, so they never run silently. Use a provider or an action instead. |
moved | ✅ | ✅ | ✅ | Honored. The move runs against state before the plan is computed — so a rename, whether of a resource, an instance key, or a whole module call, reads as a no-op instead of a create plus an orphan destroy, and a change at a moved address shows where it came from. Chains collapse, a whole-resource statement fans out to every instance, and a module rename carries everything inside it. The relocation commits as its own step when the phase applies: nothing durable changes until then, so a plan you never approve leaves the object exactly where it was. |
import block | ✅ | ✅ | ✅ | Honored. The object is adopted during the plan — the provider’s import and read become the change’s prior state — so the entry reads as a no-op or an update against the real object rather than a create, and you review the actual diff before anything is committed. for_each fan-out, computed instance keys in to, and nested-module targets all resolve. A phase may replace what it adopts, as in Terraform, and the plan says so plainly when it will. Generating configuration for a target that isn’t declared yet is coming soon. |
removed | ✅ | ✅ | ✅ | Aggregated and folded into orphan / forget planning; lifecycle { destroy = false } becomes a state-only forget. |
check | ✅ | ✅ | ⚠️➖ | Parsed but not yet evaluated — check assertions don’t run today. Coming soon. |
action + action_trigger | ✅ | ➖ | ✅ | The Terraform 1.14 action model, carried by Turf — OpenTofu has no action blocks yet. ahead of OpenTofu |
ephemeral resource | ✅ | ✅ | ✅ | Supported. A value a provider produces that participates in evaluation and never reaches state — open, renew, and close all fire, and an exported plan withholds the resource rather than serializing it. Verified end-to-end against OpenBao: every credential minted was revoked, with the audit device as an independent witness. |
| write-only arguments | ✅ | ✅ | ✅ | Supported, on both plugin protocols. This is the one channel an ephemeral value has into a managed resource: it is admitted at a write-only path and refused everywhere else, and provider responses are checked to carry null at every write-only path on plan, refresh, apply, and import. A write-only path the provider reports as requiring replacement still forces one — so rotating a secret replaces the resource instead of planning a no-op. |
List resources / list block / query | ✅ | ➖ | ➖ | The Terraform 1.14 query paradigm (list blocks, .tfquery.hcl, terraform query) for bulk discovery and import. Coming soon; OpenTofu doesn’t have it yet either. |
| resource identity (provider-declared) | ✅ | ✅ | ✅ | Provider-declared resource identity (Terraform 1.12+, OpenTofu 1.12+) is read from the provider on read / plan / apply / import and persisted in the state file itself — so a state carrying an identity object round-trips through Turf unchanged, and identity Turf writes is readable by tofu. Identity-schema upgrades are handled. It’s informational: it never changes a plan action. |
| identity-based import | ✅ | ✅ | ✅ | Import adopts a resource by either a string ID or a structured identity object, validated against the provider’s identity schema before the call so a malformed identity is reported against that schema rather than as a provider error. The import block carries the same locator — id or a structured identity, exactly one — so an identity-keyed adoption works codified as well as imperatively. |
| Meta-argument | Terraform 1.14 | OpenTofu 1.12 | Turf 0.16 | Notes |
|---|---|---|---|---|
count | ✅ | ✅ | ✅ | Fully supported on codified resources, on modules, and on a plot’s data-source declarations. A plot’s single-resource declare doesn’t model it — author via a module or the codified form (ergonomics, not a gap). Adding or removing count on a resource that already has state relocates the existing object rather than replacing it — no moved block needed. |
for_each | ✅ | ✅ | ✅ | Same as count — codified resources, modules, and a plot’s data-source declarations. Cardinality evaluation is OpenTofu’s own, so the known / deferred / error bar is exactly upstream’s: toset([]) is zero instances, a map with unknown values still enumerates, and a set with an unknown element defers rather than failing. |
enabled (module) | ➖ | ✅ | ✅ | Turf’s module-level enabled for conditional instantiation; mutually exclusive with count / for_each. Convergent with OpenTofu 1.11; absent from Terraform. |
depends_on | ✅ | ✅ | ✅ | Modules, resources, and data sources. A data source that depends on something with a pending change defers its read to the apply rather than querying an object that isn’t there yet — Terraform’s rule, followed exactly. |
lifecycle.create_before_destroy | ✅ | ✅ | ✅ | Supported; forced create-before-destroy is propagated through the plan at approval. |
lifecycle.prevent_destroy | ✅ | ✅ | ✅ | Errors if the plan would destroy the resource. |
lifecycle.ignore_changes (incl. ["all"]) | ✅ | ✅ | ◑ | Honored at preview today, but not yet consistent across every path (indexed paths and the apply path need work). Improving. |
lifecycle.replace_triggered_by | ✅ | ✅ | ✅ | Supported, with upstream’s semantics: a whole-resource reference fires on anything that leaves a different object behind, and an attribute reference fires on a value that moves, with unknown-after counting as moved. count.index / each.key resolve per instance; any other expression in an index is refused rather than guessed at, since a trigger that silently never fires is indistinguishable from a resource that didn’t need replacing. A destroy triggered across a replacement runs against the provider configuration as it stood in prior state — what makes create_before_destroy cascades safe. |
lifecycle.precondition / postcondition | ✅ | ✅ | ➖ | Custom condition checks aren’t modeled or evaluated yet — coming soon. |
lifecycle.action_trigger | ✅ | ➖ | ✅ | The 1.14 action trigger. on_failure = taint is parsed but currently behaves as halt, and condition isn’t evaluated yet — both coming soon. ahead of OpenTofu |
provider / providers = {} | ✅ | ✅ | ✅ | Resource provider = and module providers = {}, validated against configuration_aliases. A child module may use different local provider names than its caller and still resolve to the right provider. |
timeouts {} | ✅ | ✅ | ➖ | Not yet parsed or forwarded to the provider; providers run with their SDK defaults. Coming soon. |
| Feature | Terraform 1.14 | OpenTofu 1.12 | Turf 0.16 | Notes |
|---|---|---|---|---|
for-expressions, conditionals, splat, dynamic blocks, string templates, full builtin function library | ✅ | ✅ | ✅ | The whole expression language and builtin function library — Turf imposes no function allow / deny list. Composite expressions and dynamic blocks survive the declare → unit → walk round-trip. |
provider-defined functions (provider::…) | ✅ | ✅ | ✅ | Supported — verify against a provider that exports functions. |
| Feature | Terraform 1.14 | OpenTofu 1.12 | Turf 0.16 | Notes |
|---|---|---|---|---|
-target | ✅ | ✅ | ⛔ | Accepted by the schema but refused at runtime today — Turf plans the whole configuration rather than a silent partial apply. Coming soon. |
-exclude | ➖ | ✅ | ⛔ | Refused at runtime today; coming soon. (An OpenTofu-only flag — Terraform has no -exclude.) |
-refresh-only | ✅ | ✅ | ⛔ | Refused at runtime today — coming soon. |
-var-file | ✅ | ✅ | ✅ | Honored — .tfvars and .tfvars.json, applied in the order listed. Parsed once when the plan starts and carried in memory for the whole phase, never copied into the sealed plan directory. A file named explicitly that fails to parse refuses the call rather than proceeding without it. |
-replace | ✅ | ✅ | ✅ | Honored — force replacement of a target resource. |
| destroy | ✅ | ✅ | ✅ | Destroy plans the configuration plus every state orphan, reverse-dependency ordered; config files are left untouched. |
-var | ✅ | ✅ | ✅ | The top of the variables ladder (see the variable row above); TF_VAR_ environment variables and terraform.tfvars / *.auto.tfvars(.json) auto-loading are honored too. Session-supplied values stay in memory. |
| saved planfile round-trip | ✅ | ✅ | ➖ | The copy-on-approve seal directory is the plan snapshot; .tfplan file emission and ingestion are coming soon. |
provider for_each (multi-instance providers) | ➖ | ✅ | ⛔ | Turf’s provider model is strictly per-(name, alias) today, so a for_each provider can’t be routed — it refuses rather than mis-route. Coming soon. (OpenTofu-only; Terraform has no provider for_each.) |
| state / plan encryption | ➖ | ✅ | ◑ | Supported through the backend; not yet verified end-to-end. (An OpenTofu-only feature.) |
| early variable evaluation | ◑ | ✅ | ✅ | Supported. A var reference in a module source, a provider for_each, or a backend {} body resolves from the real root variable values, the way tofu init resolves them; a missing value is a diagnostic naming the variable. |
.tftest.hcl / .tofutest.hcl test runner | ✅ | ✅ | ➖ | The native HCL test files aren’t executed yet — a turf test command to run them (with plots support) is coming soon. |
| deferred changes / deferral (“try again later”) | ✅ | ◑ | ✅ | A real Terraform capability — the mechanism behind unknown count / for_each / provider config, and the substrate for Stacks. Turf’s distinctive form is a first-class, agent-driven, replan-based multi-phase convergence for any config — no experimental flag. ahead of OpenTofu |
| Construct / capability | Terraform 1.14 | OpenTofu 1.12 | Turf 0.16 | Notes |
|---|---|---|---|---|
Component config — .tfcomponent.hcl: component (wraps a module) plus stack-level variable / output / provider / removed | ✅ | ➖ | ➖ | Not parsed — Turf has no component block. Stacks is Terraform-only; OpenTofu has none of it either. Turf’s multi-environment story is workspace fan-out (below). |
Deployment config — .tfdeploy.hcl: deployment, orchestrate, identity_token, publish_output / upstream_input, store | ✅ | ➖ | ➖ | Not parsed — no deployment / orchestrate blocks. Terraform-only. |
| Capability: multi-environment, multi-round deployment | ✅ | ➖ | ◑ | A different model, not the Stacks language: one registered configuration bound by N workspaces, each with its own backend and state — the staging / prod idiom — driven across rounds by phases and deferral. Honest caveat: this is workspace fan-out, not a single object that rolls out across deployments as a unit, and there’s no orchestrate / identity_token / cross-stack output wiring yet. |
Turf is built on the OpenTofu framework — then adds an agent-oriented execution model on top. A handful of capabilities have no Terraform or OpenTofu equivalent.
turf_confirm (agent elicitation) and turf_action (sampling), served by Turf’s built-in provider identity and validated natively at plan time.
Take it slow! You're in control as infrastructure changes are applied, so the agent - and you - can intervene at any step.
Shift-left: Ad-hoc configurations graduate to idiomatic .tf via a converter.
Deferral provides a first-class, agent-driven convergence loop: a “try again later” records what to resolve, the agent resolves it, and replan re-walks until nothing remains — and no Stacks required.
Remote state backends, policy checks, private module registries, org skills — Turf slots into the Terraform automation platform you already run. Complement, not rip-and-replace.