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.
| Change | Kind | Who it breaks | What to do |
|---|---|---|---|
| base64gzip output | Breaking | Anyone with a resource argument built from base64gzip; the next plan proposes changes | Review the diff; ignore_changes temporarily if you cannot apply |
| WinRM for provisioners | Breaking (removed) | Any connection block with type winrm on remote-exec or file provisioners | Move to OpenSSH for Windows before upgrading |
| 32-bit builds (386, arm) | Last series | Runners and laptops on 32-bit CPUs; tofu init warns during the 1.13 series | Plan a move to amd64 or arm64 before 1.14 |
| macOS 13 Ventura minimum | Platform floor | Older macOS may crash or behave incorrectly | Upgrade the OS or pin the old CLI on that machine |
| convert and assume functions | New, additive | Nobody; opt in per expression | Use only where a provider guarantees the property |
| Linting and Symbol Libraries | Experimental | Nobody by default; both are opt-in and may change | Try 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.
| Function | What it promises | What it unlocks at plan time |
|---|---|---|
| assumenotnull(value) | Value is not null; errors otherwise | Comparisons 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 otherwise | Comparisons 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 argument | A module can predict a value the provider cannot, such as an ARN built from known inputs |
| assumelistlength, assumesetlength, assumemaplength | Length is within inclusive min and max bounds | Length checks return known results; min and max variants check one bound |
| convert(value, type) | Value conforms to the type constraint | An 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-localSymbol 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.
| Platform | State in the 1.13 series | What to do |
|---|---|---|
| Windows ARM64 | New official packages | Check each provider ships windows_arm64 before moving a runner |
| 386 and arm (32-bit) | Last series; tofu init warns | Migrate to amd64 or arm64; 1.14 has no official packages for them |
| macOS 13 Ventura or later | Supported floor | Verify developer laptops and any self-hosted Mac runners |
| macOS before 13 | May crash or misbehave | Do 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.
- Search the repository for type winrm connections and for base64gzip usage.
- Check the platform of every runner and laptop against the table in section six.
- Upgrade in a dedicated branch with tofu init -upgrade and save the plan.
- Confirm the only changes are base64gzip-driven; apply them, or use ignore_changes for one release and track its removal.
- 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=allThe 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.
Sources
- OpenTofu blog: OpenTofu v1.13.0 release announcement
- OpenTofu docs: the assume family of functions
- OpenTofu docs: the convert function
- OpenTofu docs: the base64gzip function
- OpenTofu blog: a vision for built-in linting
- OpenTofu blog: an introduction to Symbol Libraries
- GitHub: OpenTofu v1.13.0 release page

