10 min readCJ Brewer

Anatomy of a `.swirls` file

One .swirls file from the Swirls team's own GTM stack, read top to bottom. An OIDC-federated identity, a schedule, a graph with a scoped-delegated HTTP call, a durable Postgres-backed stream, and a trigger. One file. Five primitives. No long-lived API tokens. This is what a deployable agent looks like.

swirlsagentsarchitecture

There is a file in the Swirls team's GTM repo that searches GitHub for repos matching our ICP, normalizes the results into lead records, and writes them to a durable, queryable Postgres-backed stream. Every morning at 07:30 UTC the runtime executes it. Nobody on the team logs in to push a button. Nobody wrote a service to wrap it. Nobody pasted a long-lived GitHub token into a .env file.

The file is the agent. Not a service that runs it. Not a wrapper around a model call. The file.

This post is a reading of that file. We will go top to bottom, one block at a time, and at each block name the production property the runtime provides.

#Read this file

Here is the outline, block headers only:

auth gtm_github { ... }
schedule gtm_github_schedule { ... }

graph gtm_github_discovery {
  root { ... }
  node fetch { ... }
  node pack { ... }
  flow { ... }
}

stream gtm_leads_github { ... }
trigger gtm_github_on_schedule { ... }

Five blocks. Each one is a primitive the runtime understands. Top to bottom, then: here is what each block does.

#The auth block

auth gtm_github {
  label: "GitHub via Swirls OIDC federation"
  type: cloud
  provider: "github"
  connection_id: "swirls_managed_github"
}

This is identity as a primitive, and specifically the federated form. The auth block names a credential bundle the runtime can scope to a single HTTP call. The provider plus connection_id is what the runtime needs to fetch a short-lived token at call time. The block is reusable across any number of nodes in the file.

Notice what is not in this file. There is no secret block declaring GITHUB_TOKEN. There is no process.env.GITHUB_TOKEN waiting in production. There is no leaked-token rotation playbook. Swirls handles the OAuth dance with GitHub on the file's behalf; the file just names the connection.

This matters more in agent systems than it does in traditional services. When an LLM is choosing which tool to call, a long-lived API token in the process is a liability that outlives the request that exposed it. An OIDC-federated credential is minted per call, scoped to the call, and short-lived by construction. The .swirls file gets the security property without paying the usual cost of OAuth client setup, because the runtime owns that part.

The auth type here is cloud (the spec's managed-connection form). The other valid types are oauth (raw OAuth where the file holds the client credentials), api_key, basic, and bearer. Use cloud whenever the provider is one Swirls can federate to. Use the others only when you must.

#The schedule

schedule gtm_github_schedule {
  label: "GTM GitHub repo search"
  cron: "30 7 * * *"
  timezone: "UTC"
  enabled: true
}

Time is a first-class trigger source. The schedule is declared at the file level rather than wired up in a separate cron.yaml or a parent service. When the agent ships, the schedule ships with it.

#The graph

The graph is where the work happens. Three nodes and a flow block:

graph gtm_github_discovery {
  label: "Search GitHub for workflow-related repos"

  root {
    type: code
    label: "Query params"
    outputSchema: @json {
      {
        "type": "object",
        "required": ["discoveredAt", "source", "q"],
        "properties": {
          "discoveredAt": { "type": "string" },
          "source": { "type": "string" },
          "q": { "type": "string" }
        },
        "additionalProperties": false
      }
    }
    code: @ts {
      return {
        discoveredAt: new Date().toISOString(),
        source: 'github',
        q: 'workflow+engine+OR+agent+OR+orchestration+language:TypeScript',
      }
    }
  }
  ...
}

Every graph has exactly one root node. This one is a code node that computes the query parameters for the GitHub search. The outputSchema is the contract: the runtime validates the return value against the JSON Schema before the next node runs. If the shape is wrong, the run fails at the boundary. The error does not surface three nodes later when something downstream tries to read a missing field. Schema validation at every node boundary is what makes a graph composable: each node is a small typed function the next node can rely on.

The HTTP call is next, and this is the load-bearing line of the file:

node fetch {
  type: http
  label: "GitHub search API"
  method: GET
  auth: gtm_github
  url: @ts {
    const q = context.nodes.root.output.q
    return (
      'https://api.github.com/search/repositories?q=' +
      encodeURIComponent(q) +
      '&per_page=15&sort=updated'
    )
  }
}

auth: gtm_github. One line. This is scoped delegation: the runtime fetches a fresh short-lived GitHub token at call time and hands it to this one HTTP call.

The node does not see other secrets. The graph does not see the credential. The credential is bound to the call. The process does not hold it. If the agent grows a second HTTP node tomorrow that talks to a different service, that node's auth: reference is the only thing that decides which credential reaches the wire.

The third node is another code node that turns the GitHub API response into a uniform leads array (domain, company name, snippet, source metadata) ready to write downstream. Then the flow block connects the nodes:

flow {
  root -> fetch
  fetch -> pack
}

One edge per line. No conditionals at the edge level (conditional routing uses a switch node). The graph is a DAG, and the DAG is the agent's control flow.

#The stream

stream gtm_leads_github {
  label: "GTM leads from GitHub"
  graph: gtm_github_discovery
  enabled: true

  schema: @json { ... }

  condition: @ts {
    const p = context.output.pack
    return Array.isArray(p?.leads) && p.leads.length > 0
  }

  prepare: @ts {
    return context.output.pack
  }
}

This is the state-of-record. A top-level stream block subscribes to a graph (graph: gtm_github_discovery), defines the shape of what gets written (schema), decides whether a given run produces a row (condition), and shapes the row from the graph's output (prepare). Subscribing here is literal: every time the runtime finishes a successful run of the named graph, it evaluates the stream's condition, and if the condition returns true it writes one row using whatever prepare returns.

Every successful run that satisfies the condition leaves exactly one row in gtm_leads_github. The stream is materialized in Postgres. A SELECT over gtm_leads_github is a real query the operator can run, and it is the same data the runtime uses when a downstream graph reads from this stream as input.

The agent's history is not a log file. It is a queryable table.

#The trigger

trigger gtm_github_on_schedule {
  schedule:gtm_github_schedule -> gtm_github_discovery
  enabled: true
}

One line of binding syntax. The schedule fires; the graph runs; the stream collects. Triggers are declarative. They are not buried in application middleware or wired up in a separate event-routing service. The binding is part of the artifact, which means there is no separate "wire it up in production" step.

#What's not in this file

The format scales. The blocks this file does not use, but could:

  • review: blocks on any node: the runtime pauses execution until a human approves, with a typed form schema for the reviewer (see review-workflow.swirls in the canonical examples)
  • graph nodes that call a subgraph as a tool, under scoped delegation
  • parallel nodes for fan-out across search, extract, or findall operations
  • postgres blocks at the top level, paired with postgres nodes that run parameterized SELECT or INSERT against external databases
  • form and webhook blocks instead of (or alongside) a schedule
  • secret blocks and the api_key / basic / bearer / raw oauth auth types, for providers Swirls cannot federate to yet

Same artifact format. Different slots filled.

#You just read an agent

What you read was not a route handler. It was not a Lambda function with a package.json next to it. It was not a config file for an app that calls a model. It was the agent.

The runtime executes this file locally and in the cloud. The same lines you reviewed in a PR are what runs. The credential is minted per call. The state is in Postgres. The schedule lives in the file. The whole thing is one artifact. If you want to see more .swirls files, swirls.ai is where to start.


A note on the example: the version of this file currently running in our GTM stack uses a bearer-token auth block against a stored GitHub PAT. The OIDC federation form shown above is what the file becomes when GitHub federation lands in the next few weeks. Everything else in the file is what we run today.