Labs
All posts

Manage Runlayer Agents as code with Terraform

Runlayer Agents can now be defined in Terraform: the prompt, model, tools, and network access live in your repo, and every change ships through a reviewed plan.

Agents as codeRunlayer Team6 min read

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_agent resource defines an agent, with the prompt kept as a file in your repo.
  • Every change to an agent shows up as a terraform plan diff 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:

main.tfHCL
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:

prompts/support-triage.mdMarkdown
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.

01 · GITPull requestprompt, tools,model, policy02 · PLANterraform planthe exact diff,posted for review03 · REVIEWHuman approvalsame rules asany infra change04 · APPLYterraform applyone API callper changed agent05 · RUNLAYERAgent updatedgoverned, audited,live on next rungit revert is the rollback
Fig. 1. An agent change takes the same path as every other change to production.

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:

terraform planPlan
  ~ 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:

release-notes.tfHCL
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:

team-agents.tfHCL
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.

IN YOUR REPO · TERRAFORMWhat the agent isname, prompt, modelconnectors and their exact toolsskills, subagents, memory, built-in toolsnetwork access policy, environmenttags, visibility, on/off switchIN THE APPWhat happens when it runsschedules, triggers, and webhookssharing with people and teamsusage limits and budgetssessions, runs, and artifactsthe description Runlayer writesboth sides sit under the same policies and audit log
Fig. 2. Terraform owns the definition. Operators own the schedule, sharing, and budget.

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_agent data source exposes tool_count, skill_count, and env_keys for 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.
Bring an existing agent under TerraformShell
$ 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.

Runlayer is the leading AI control plane, with AI enablement, security, and control in one platform.