Platform teams already manage servers, databases, IAM roles, and DNS as code. Every production change goes through a pull request, someone reviews the diff, and Git keeps the history. Agents have mostly skipped that process. Someone edits a prompt in a text box, adds a tool because a run failed, and the change goes live with no review and no record of why.
That’s fine for one agent. It breaks down at fifty, when an AI transformation lead has to say which agents can write to Salesforce, who changed the support agent’s instructions last week, and how to undo it.
Runlayer Agents can now be defined in Terraform. The Runlayer provider’s runlayer_agent resource covers everything that defines an agent: its prompt, model, connectors and their exact tools, skills, subagents, memory, network access policy, and environment. Your agents get the same review and history as the rest of your infrastructure.
In short
- One
runlayer_agentresource defines an agent, with the prompt kept as a file in your repo. - Every change to an agent shows up as a
terraform plandiff someone can review before it ships. - Least privilege is written down: the exact tools per connector, and the hosts the agent can reach.
- Existing agents can be imported, and the provider is available today in beta.
Your first agent in one file
The provider installs from Runlayer’s registry with a plain terraform init and authenticates with a user API key. Here’s a support triage agent that can search and read Linear issues, and nothing else:
terraform { required_providers { runlayer = { source = "downloads.runlayer.com/runlayer/runlayer" version = "~> 1.0" } } } # Reads RUNLAYER_API_KEY and RUNLAYER_BASE_URL from the environment. provider "runlayer" {} data "runlayer_server" "linear" { server_id = var.linear_server_id } resource "runlayer_agent" "support_triage" { name = "support-triage" prompt = trimspace(file("${path.module}/prompts/support-triage.md")) tags = ["managed-by-terraform", "support"] # Exactly the tools this agent needs. An empty list means none. servers = [{ server_id = data.runlayer_server.linear.id tool_names = ["search_issues", "get_issue"] }] example_prompts = [ "Triage the newest ticket in the support queue.", "Is ticket SUP-1234 a duplicate?", ] # Start parked. Flip to false once you've tried it in the app. is_disabled = true }
After terraform apply, the agent appears in Runlayer under Agents. It starts disabled, so nothing can trigger it until you’ve tried it in the app and flipped is_disabled to false in a second, reviewed change.
Prompts are code too
An agent’s prompt carries much of its behavior. It deserves the same treatment as any other file that decides what production does. Keep it in the repo, next to the agent that uses it:
You are the support triage agent. For every incoming ticket: 1. Search Linear for duplicates before doing anything else. 2. Classify severity (P0-P3) using the playbook, and say why in one line. 3. Draft a reply for the support engineer to send. Never send it yourself. If you are unsure about severity, pick the higher one.
Now a prompt change is a pull request. Reviewers see which line changed and why, and git blame tells you who wrote it.
Review the diff, not the dashboard
Say someone wants the triage agent to link duplicates and to comment on Linear issues. Here is the plan a reviewer sees:
~ resource "runlayer_agent" "support_triage" { id = "9f1c2b7e-..." name = "support-triage" ~ prompt = <<-EOT ... - 3. Draft a reply for the support engineer to send. + 3. Draft a reply, and link the duplicate ticket if you found one. EOT ~ servers = [ ~ { ~ tool_names = [ "get_issue", "search_issues", + "create_comment", ] }, ] } Plan: 0 to add, 1 to change, 0 to destroy.
A reviewer can see what this change does. The agent will start writing to Linear, through one new tool. That gets a conversation before it ships, not after an incident. If the change turns out wrong, revert the commit and apply again.
Least privilege you can read
Each connector on an agent takes an explicit tool list, and an empty list means no tools, not all of them. The agent’s network access policy goes in the same block, so the hosts the agent can reach are in the repo too:
resource "runlayer_agent" "release_notes" { name = "release-notes" prompt = trimspace(file("${path.module}/prompts/release-notes.md")) model = "claude-sonnet-5" servers = [{ server_id = runlayer_server.github.id tool_names = ["list_pull_requests", "get_pull_request"] }] # The sandbox can reach GitHub and nothing else on the internet. egress_policy = { default_action = "deny" allow = ["github.com", "*.githubusercontent.com"] } # Sensitive in plan output. The API never returns the values. env = { RELEASE_CHANNEL = "stable" } }
Security teams can review agent access the way they review IAM, in a file. The same Terraform configuration can manage the connectors themselves with runlayer_server and the policies around them with runlayer_policy.
One pattern, many agents
Once the first agent works, the next ten follow the same pattern. A map and a for_each give a team a set of agents that share conventions, with each one’s prompt in its own file:
locals { agents = { support-triage = { tools = ["search_issues", "get_issue"] } bug-dedupe = { tools = ["search_issues", "get_issue", "create_comment"] } sprint-digest = { tools = ["list_issues"] } } } resource "runlayer_agent" "team" { for_each = local.agents name = each.key prompt = trimspace(file("${path.module}/prompts/${each.key}.md")) tags = ["managed-by-terraform", "support"] servers = [{ server_id = data.runlayer_server.linear.id tool_names = each.value.tools }] }
The same approach works for promoting agents between workspaces. Point the provider at staging, apply, check the agent, then apply the same configuration to production.
What stays in the app
Terraform defines what an agent is. How it runs day to day stays in Runlayer, where the people operating the agent can change it without a deploy.
A few details are useful to know before you start:
- Some fields are write-only. Terraform sends connectors, skills, and environment variables on every apply but doesn’t read them back, so edits made to those in the app don’t show up as drift. The
runlayer_agentdata source exposestool_count,skill_count, andenv_keysfor checks in CI. - Imports are one command. The agent ID is the UUID in its URL. The first plan after an import re-sends connectors, skills, and environment from your configuration.
- Ownership follows the key. An agent created by Terraform is owned by the user whose API key applied it. Organization API keys can’t create agents.
$ terraform import runlayer_agent.support_triage 9f1c2b7e-...Start with one agent
Pick the agent your team changes most often, import it, and put its prompt in a pull request. The next change to it gets a plan, a review, and a history.
Runlayer is the leading AI control plane, with AI enablement, security, and control in one platform. The Terraform provider is available today in beta for Runlayer-hosted and self-hosted deployments, with the full schema under Help → Terraform Reference in the app. See how it fits your platform → book a custom demo.