Skip to main content
BlogDevOps

OpenTofu 1.13: convert, assume and Upgrade Breaking Changes

OpenTofu 1.13 upgrade notes: base64gzip output change, WinRM removal, convert and assume functions, experimental linting, Windows ARM64 and 32-bit end.

Matthews Wong

Software Engineer

October 202611 min read
OpenTofu 1.13: convert, assume and Upgrade Breaking Changes

Key Takeaway

OpenTofu 1.13, announced on 29 September 2026, is a deliberately light release: new convert and assume functions, experimental linting and Symbol Libraries, and Windows ARM64 packages. The upgrade risks are base64gzip output changing, WinRM provisioner connections being removed, macOS 13 as the minimum, and the last series with 32-bit builds.

Most release posts are read for the feature list. For 1.13 the feature list is short and the upgrade notes are what will actually cost you an afternoon, so this post reads them in that order of importance.

I read the official 1.13.0 announcement, the function reference pages for convert, the assume family and base64gzip, the two experiment posts and the GitHub release page. I have not run 1.13 against production state, so the code below is shaped by those documents and is not measured output. Anything I could not find in them is left out.

A light release, on purpose

The announcement says the development period was shortened so that OpenTofu lines up with the Go release cycle, and it calls 1.13 relatively light on new features. The 1.13 series gets security support until August 2027.

Light does not mean uneventful. A Go upgrade underneath the binary is what changes base64gzip output, and the removals follow the usual deprecate-then-remove path. Here is every change I could confirm, sorted by whether it can hurt you.

ChangeKindWho it breaksWhat to do
base64gzip outputBreakingAnyone with a resource argument built from base64gzip; the next plan proposes changesReview the diff; ignore_changes temporarily if you cannot apply
WinRM for provisionersBreaking (removed)Any connection block with type winrm on remote-exec or file provisionersMove to OpenSSH for Windows before upgrading
32-bit builds (386, arm)Last seriesRunners and laptops on 32-bit CPUs; tofu init warns during the 1.13 seriesPlan a move to amd64 or arm64 before 1.14
macOS 13 Ventura minimumPlatform floorOlder macOS may crash or behave incorrectlyUpgrade the OS or pin the old CLI on that machine
convert and assume functionsNew, additiveNobody; opt in per expressionUse only where a provider guarantees the property
Linting and Symbol LibrariesExperimentalNobody by default; both are opt-in and may changeTry in a branch, give feedback, do not depend on them

Windows on ARM64 is also new as an officially supported platform, which is good news and, as section six explains, comes with a provider caveat.

base64gzip: equivalent output, different bytes

The base64gzip function gzips a string and Base64-encodes the result. The announcement says an upstream Go standard library change makes the result equivalent to earlier versions but not byte for byte identical, and the function reference repeats that. Since OpenTofu compares configuration values to state as exact strings, any resource argument built from it will look changed.

The documented escape hatch is the ignore_changes meta-argument: keep the previous value for now and remove the block when you are ready to adopt the new encoding. The announcement also mentions that a third-party provider built with Go 1.26 or earlier could supply the old behaviour, that OpenTofu ships no official provider for it, and that you can search the OpenTofu Registry. I would treat that as a last resort.

resource "aws_s3_object" "index" {
  bucket           = aws_s3_bucket.site.id
  key              = "index.html.gz"
  content_base64   = base64gzip(file("${path.module}/index.html"))
  content_encoding = "gzip"

  # Temporary: the 1.13 encoding is equivalent but not byte-for-byte identical,
  # so the first plan after upgrading proposes an in-place update. Pin the old
  # value, upgrade, then delete this block in its own reviewed change.
  lifecycle {
    ignore_changes = [content_base64]
  }
}

The failure mode to watch is a reviewer seeing a wall of in-place updates on objects whose content did not change and approving them blindly. Run the upgrade in its own pull request so the only diff is this one, and make removing ignore_changes a separate, labelled change.

WinRM is gone from provisioners

WinRM was deprecated in 1.12.0 and is now removed for the remote-exec and file provisioners; a connection block with type winrm is an error. The announcement recommends migrating to OpenSSH for Windows before you upgrade. The ssh connection type below is the standard alternative, but whether sshd is installed and reachable on your Windows hosts is the real work.

# Wrong: errors in 1.13 (deprecated in 1.12, removed for remote-exec and file).
resource "null_resource" "bootstrap" {
  connection {
    type     = "winrm"
    host     = var.windows_host
    user     = "Administrator"
    password = var.admin_password
  }
  provisioner "remote-exec" {
    inline = ["powershell -File C:\\bootstrap.ps1"]
  }
}

# Right: the same connection over OpenSSH for Windows, once sshd is running on the host.
resource "null_resource" "bootstrap" {
  connection {
    type        = "ssh"
    host        = var.windows_host
    user        = "Administrator"
    private_key = file(var.ssh_key_path)
  }
  provisioner "remote-exec" {
    inline = ["powershell -File C:\\bootstrap.ps1"]
  }
}

If the Windows image comes from a golden template, enable OpenSSH there, not in a provisioner that needs WinRM to do it. You would be bootstrapping the new path with the path that no longer exists.

convert and assume: fewer unknowns at plan time

Values that are only known after apply show as known after apply in the plan, and several features refuse them during planning, including enabled, for_each and count. The new functions let a module author tell OpenTofu what it cannot infer.

convert declares the type, so an unknown result still has a known shape. The assume family declares a property of the value, so comparisons and checks can run at plan time. The reference lists these signatures and says the hints are covered by the compatibility promises, with earlier detection being best effort.

FunctionWhat it promisesWhat it unlocks at plan time
assumenotnull(value)Value is not null; errors otherwiseComparisons to null return a known boolean, if the type is known (pair with convert if not)
assumestringprefix(string, expected_prefix)String starts with the prefix; errors otherwiseComparisons to an empty string and startswith calls return known results
assumeequal(actual_value, expected_value)Converts to the second argument's type, checks equality, returns the second argumentA module can predict a value the provider cannot, such as an ARN built from known inputs
assumelistlength, assumesetlength, assumemaplengthLength is within inclusive min and max boundsLength checks return known results; min and max variants check one bound
convert(value, type)Value conforms to the type constraintAn unknown value keeps a known shape, so attribute access has a known type
# 1. convert: tell OpenTofu the SHAPE of a value it cannot see until apply.
locals {
  vendor = convert(
    jsondecode(something_dynamic.example.output_json),
    object({ id = string, tags = map(string) })
  )
  # local.vendor is still unknown, but local.vendor.id is known to be a string.
}

# 2. assume...: promise a property of the value, so downstream checks run at plan.
variable "vpc_id" {
  type = string

  validation {
    # Without the hint, an unknown vpc_id makes this rule wait for apply.
    condition     = startswith(var.vpc_id, "vpc-")
    error_message = "vpc_id must look like vpc-..."
  }
}

module "network_checks" {
  source = "./modules/network-checks"

  # Only safe because the provider documentation guarantees the prefix.
  vpc_id = assumenotnull(assumestringprefix(aws_vpc.example.id, "vpc-"))
}

Note the ordering in the example: assumenotnull wraps assumestringprefix because validation rules usually check for null first. The reference says assumenotnull only works when the type is already known, which is exactly the case convert covers.

The two experiments, and how to switch them on

Linting is switched on per command with the lint option. The linting post lists four built-in rules in 1.13: core:no-type-variable, core:count-instead-enabled, core:unused-variable and core:unused-local. It also says a configuration-file approach is planned but not defined yet, so the flag is the only documented route today.

# Every built-in rule, as warnings, on any of the three commands.
tofu validate -lint=all
tofu plan -lint=all

# Or pick rules by ID. These four are the ones the linting post says 1.13 ships.
tofu plan -lint=core:no-type-variable,core:count-instead-enabled,core:unused-variable,core:unused-local

Symbol Libraries are reusable collections of functions, types and constants in files ending in sym.hcl, imported with a symbols block and enabled through an experiments list in project configuration. The introduction post says libraries cannot read resource or data state or use provider functions, though resource values can be passed in as arguments.

# project configuration: opt in to the experiment explicitly
language {
  experiments = [symbol_libraries]
}

# Import a directory of *.sym.hcl files under the label "lib".
symbols "lib" {
  source = "./example-lib"
}

# Functions and types: symbols::lib::<name>()    Constants: symbols.lib.<name>

The Symbol Libraries post says the goal is to stabilise for 1.14 and asks for feedback through a GitHub discussion. That is an intention, not a promise, so keep libraries out of anything you cannot rewrite.

Platforms: Windows ARM64 in, 32-bit and old macOS out

Windows on ARM64 gets official packages, and the announcement says most OpenTofu builds of the major cloud providers have windows_arm64 packages. Third-party provider maintainers set their own platform policy, so one missing provider can block an ARM64 Windows runner.

PlatformState in the 1.13 seriesWhat to do
Windows ARM64New official packagesCheck each provider ships windows_arm64 before moving a runner
386 and arm (32-bit)Last series; tofu init warnsMigrate to amd64 or arm64; 1.14 has no official packages for them
macOS 13 Ventura or laterSupported floorVerify developer laptops and any self-hosted Mac runners
macOS before 13May crash or misbehaveDo not upgrade the CLI there; replace the machine or OS

The 32-bit warning is the one that quietly matters: a Raspberry Pi style ARM runner can keep working through 1.13 and then have no official upgrade path.

Where it goes wrong: hints that lie, and experiments that move

The assume functions are promises, and the announcement is explicit about the cost of breaking one. If you promise a vpc- prefix and the value turns out to be subnet-, the call fails during apply, which is later and more painful than the unknown it replaced.

Only use an assume function when provider or vendor documentation guarantees the property. A wrong hint turns a plan-time unknown into an apply-time failure, possibly halfway through a change.

  • Detection timing can differ between versions: a problem caught at plan in one release may only show at apply in another, and the reverse. Do not build a guarantee on it.
  • The Symbol Libraries language elements are described as a starting point that may change with feedback.
  • Linting is early infrastructure with four rules; a clean run proves little.
  • ignore_changes for base64gzip is a debt. Leave it in and the resource silently stops tracking real content changes.

My rule: take the additive features slowly and the removals seriously. The first group can wait a release; the second will fail a pipeline on the first run.

What to do on Monday

None of this needs a migration project. It needs one afternoon and a branch.

  1. Search the repository for type winrm connections and for base64gzip usage.
  2. Check the platform of every runner and laptop against the table in section six.
  3. Upgrade in a dedicated branch with tofu init -upgrade and save the plan.
  4. Confirm the only changes are base64gzip-driven; apply them, or use ignore_changes for one release and track its removal.
  5. Run tofu validate with the lint option on one module and tell the project what you found.
# 1. Find the three things that break before you touch the binary.
grep -rn 'type *= *"winrm"' --include='*.tf' .
grep -rn 'base64gzip' --include='*.tf' .
tofu version            # note the platform line: windows_386 / linux_arm / darwin on macOS < 13

# 2. Upgrade in a branch, then read the plan before anything else.
tofu init -upgrade
tofu plan -out=tfplan-1.13

# 3. Only base64gzip arguments should show as changes. Anything else is a real diff.
tofu show tfplan-1.13 | grep -B2 -A6 'base64gzip\|content_base64'

# 4. Try the linter on the module you know best.
tofu validate -lint=all

The commands below are the first hour of that afternoon. Step three greps the saved plan, which is a convenience, not a guarantee, so still read the diff.

The rule to carry: with a light OpenTofu release, the notes under breaking changes are the whole release. Fix WinRM and base64gzip in their own change, check your platforms, and treat convert, assume, linting and Symbol Libraries as things to try in a branch rather than to build on.

FAQ

Frequently Asked Questions

Short answers to what readers ask most about this topic.

What is new in OpenTofu 1.13?

OpenTofu 1.13 adds official Windows ARM64 packages, convert and assume functions that reduce unknown values during planning, an experimental linting option, and an experimental Symbol Libraries feature. The announcement describes it as a relatively light release aligned with the Go release cycle.

What breaks when I upgrade to OpenTofu 1.13?

Four things: base64gzip output is equivalent but not byte for byte identical so plans may show changes, WinRM is removed for the remote-exec and file provisioners, macOS 13 or later is required, and this is the last series with 32-bit builds. Check each against your repositories and runners before upgrading.

How do I stop base64gzip changes showing in my plan after upgrading?

Review the proposed changes first, since the content is equivalent. If you cannot apply them yet, the documentation says you can temporarily use the ignore_changes meta-argument to keep the previous value, then remove it when you are ready to adopt the new encoding.

What do the OpenTofu assume functions do?

They let a module author promise a property of a value that is only known after apply, such as being non-null or having a given string prefix. OpenTofu can then evaluate checks during plan, but if the promise is wrong the call fails during apply, so use them only where documentation guarantees the property.

How long is OpenTofu 1.13 supported?

The announcement says the v1.13 series receives security support until August 2027. It is also the last series with official 32-bit packages, so plan any 386 or arm migration well before 1.14.