Post

GitHub Agentic Workflows: Everything You Need to Know

Alex Garrett-SmithNov 913 min read
GitHub Agentic Workflows: Everything You Need to Know

GitHub Agentic Workflows went generally available on 28 October. You describe a job in a Markdown file ("summarise last week's activity", "explain why CI failed"), it gets compiled into a normal GitHub Actions workflow, and a coding agent does the work inside your runner without write access to your repo.

Let's take a look at how it works, build one small workflow, go through each safety layer, and figure out what it'll cost before you switch anything on.

What it is

An agentic workflow is a Markdown file that lives in .github/workflows/. The YAML frontmatter at the top holds everything Actions needs to know (the trigger, permissions, which agent to use, what it's allowed to write), and the body underneath is plain English instructions for the agent.

You run gh aw compile and it generates a .lock.yml file next to it, which is the actual Actions workflow that runs, and you commit both.

It's open source (MIT), built by GitHub Next, and available on github.com for Free, Team and Enterprise plans. The default engine is GitHub's Copilot CLI, which needs a Copilot licence, but you can swap in Claude, Codex and a few others with your own API key (more on that later).

Getting set up

First, install the gh CLI extension:

Text
gh extension install github/gh-aw

If you've never used it before, the quickest way to see one working is to add one of GitHub's ready-made workflows from the agentics repo. From inside your repo, run:

Text
gh aw add-wizard githubnext/agentics/daily-repo-status

The wizard is interactive. It asks you to pick an engine (Copilot is the default), helps you set up the secret that engine needs, and compiles the workflow. After that, it opens a pull request that adds the .md file and its .lock.yml to .github/workflows/ (or writes them locally, if you'd rather), and offers to run it straight away.

Merge that and you've got an agent writing a daily status report for your repo as an issue.

If you'd rather start from scratch, gh aw new gives you a template to fill in:

Text
gh aw new stale-prs

That creates .github/workflows/stale-prs.md, which is what we'll write next.

Writing your first workflow

Let's build something small and useful: a weekly issue that lists pull requests nobody has touched in a while. It only reads from the repo and can only write one thing (a single issue), which makes it a nice safe first workflow.

Here's the whole file:

Text
---
on:
  schedule: weekly on monday
  workflow_dispatch:

permissions:
  contents: read
  pull-requests: read

tools:
  github:
    toolsets: [repos, pull_requests]

network: defaults

safe-outputs:
  staged: true
  create-issue:
    title-prefix: "[stale prs] "
    labels: [report]
    max: 1
---

# Stale pull request summary

Find every open pull request in this repository that hasn't had a commit,
comment or review in the last 14 days.

For each one, list:
- the title, with a link to it
- who opened it, and how many days since the last activity
- one sentence on what it's waiting for (a review, changes from the author,
  a failing check)

Group them by what they're waiting for. If there aren't any stale pull
requests, don't create an issue.

Let's go through the frontmatter.

on

The on block works like any other Actions trigger, with a few extras.

schedule: weekly on monday is one of gh-aw's fuzzy schedules. Rather than you picking a cron time, the compiler picks one for you based on the repo and the file's path, so every workflow out there doesn't fire at midnight at the same time. The time stays the same each time you recompile.

I've also added workflow_dispatch, so we can run it by hand while we're testing.

permissions

These are normal Actions permission scopes, and everything here is read. The agent can look at code and pull requests, but it can't change anything directly. That's true even though the job ends up creating an issue, which we'll come to in a moment.

tools

github: gives the agent GitHub's MCP tools so it can list and read pull requests. On its own, github: turns on a default set of toolsets that includes issues, and the compiler will warn you that you're missing issues: read. This job doesn't need to read issues, so I've narrowed it down to the repos and pull_requests toolsets instead.

There are a bunch of other tools you can switch on here, like edit: for changing files, bash: for running commands (a short list of safe ones like ls, cat and grep by default), web-fetch: and playwright:. You'd only add what the job actually needs, and this one only needs to read GitHub.

network

defaults only lets the agent reach basic infrastructure, like certificate checks and Ubuntu's package mirrors. This is also what you get if you leave network out. If you needed it to reach an API or install Python packages, you'd list them under network.allowed, and anything not on that list is blocked.

safe-outputs

This is the only way the agent can change anything. We're allowing one issue, with a title prefix and a label so it's obvious where it came from.

The staged: true line means that for now it won't even create that issue. It'll write a preview of what it would have done to the Actions step summary instead, which is what you want for the first few runs.

The body

The Markdown body is the prompt. I'd keep it short and specific, and tell it what to do when there's nothing to report, otherwise you'll end up with an issue every Monday saying "no stale pull requests found".

Compile, commit and run

Once you're happy with the file, compile it:

Text
gh aw compile

This validates the frontmatter and writes .github/workflows/stale-prs.lock.yml. Commit and push both files, since Actions runs the lock file and gh-aw needs the Markdown:

Text
git add .github/workflows/stale-prs.md .github/workflows/stale-prs.lock.yml
git commit -m "Add stale PR summary workflow"
git push

Now trigger it, rather than waiting until Monday:

Text
gh aw run stale-prs

You can check on it with gh aw status, and once it's finished, gh aw logs stale-prs downloads the logs along with how long each run took, how many tokens it used, what it cost and how many turns the agent took. For a deeper look at a single run, gh aw audit <run-id> gives you a full report, including a firewall section showing which domains the agent tried to reach and whether they were allowed or blocked.

Editing it later

When you change the frontmatter (the trigger, permissions, tools and so on), you need to run gh aw compile again and commit the new lock file.

Changes to the Markdown body don't need recompiling, though. The body gets loaded when the workflow runs, so you can tweak the prompt, push, and the next run picks it up.

What's in the lock file

It's worth opening the .lock.yml once, because it's a lot longer than the Markdown you wrote (ours compiles to around 2,000 lines). Behind the scenes, your one workflow becomes a chain of separate jobs, each with its own permissions.

  1. activation prepares the prompt, checks the lock file still matches the Markdown, and checks you haven't gone over your daily AI budget.
  2. agent checks out your repo, starts the MCP servers in their own containers, runs the agent, removes any secrets from its output, and uploads what the agent asked to do as an artifact. This job only gets the read permissions from your frontmatter.
  3. detection reads that output and looks for anything suspicious.
  4. safe_outputs makes the changes. In our example it's the only job that gets issues: write, apart from gh-aw's own housekeeping in activation and conclusion, which can open an issue to tell you a run failed or that your compiled version needs updating.
  5. conclusion sums it all up. If your workflow is triggered by an event, like a new issue, you'll also get a pre_activation job at the start. It checks whether the person who triggered it has the right role before anything else runs.

The compiler pins every action it uses to a commit SHA and every container image to a digest, so nothing can change underneath you between compiles. You can also run security scanners over the result with gh aw compile --actionlint --zizmor --poutine. Don't edit the lock file by hand, since your changes will be gone the next time you compile.

The safety layers

Handing an agent your repo sounds a bit scary, so it's worth understanding what's stopping it from doing something you didn't ask for. GitHub's changelog lists five layers, so let's go through them one at a time.

Integrity filtering

This controls which GitHub content the agent can see at all, based on who wrote it. It's set with tools.github.min-integrity, and the options are merged, approved, unapproved and none.

On public repos, if you don't set anything, it defaults to approved, which filters out content from untrusted authors before the agent reads it. Private and internal repos don't get a filter applied automatically. (If you've seen lockdown: true in older examples, it's been replaced by this.)

Read-only permissions

As we saw above, the agent job runs with read permissions. Even if something convinces the agent to try and push code or close every issue, it doesn't have a token that can do it.

A sandbox and a firewall

The agent runs in a container on its own network behind the Agent Workflow Firewall, a proxy that only lets traffic through to the domains in your network block. Your model API keys sit in a separate proxy too, so the agent never sees them.

If you want more isolation than a container, there are newer micro-VM runtimes (Cloud Hypervisor and NVX), although they're still marked as preview in the docs.

Safe outputs

This is the one that does most of the work. The agent can't write to GitHub itself. It can only ask, in a structured format, for one of the actions you've listed under safe-outputs.

A separate job then checks the request against your rules (how many, which labels, what title prefix) and only then makes the API call with its own scoped token. So if you've allowed one issue, the most it can ever do is create one issue.

There are a load of output types beyond create-issue, like add-comment, add-labels, create-pull-request and push-to-pull-request-branch, and most have limits you can tighten.

For example, add-labels takes an allowed list so the agent can't invent its own labels. Pull requests it opens aren't merged automatically either, unless you opt in. There's an experimental merge-pull-request output and an --auto-merge-prs flag on gh aw run, but I'd leave both well alone.

Threat detection

Before the safe output job runs, a separate detection job uses AI to scan the agent's output and any code changes for prompt injection, leaked secrets and malicious code. It's switched on automatically whenever you have safe outputs.

By default it runs in warn mode. If it finds something, it flags outputs that a human will review anyway (like a pull request) and stops the ones nobody would see. To make it block everything instead, set continue-on-error: false under safe-outputs.threat-detection.

It's a useful extra check, but it's an AI reading the agent's output, so it can miss things. I wouldn't rely on it alone, and that's why the other four layers exist.

Untrusted input

Anything a stranger can type into your repo is untrusted, and that includes issue titles, issue bodies, PR descriptions and comments. If your workflow triggers on new issues, someone can write an issue that's really instructions for your agent.

Integrity filtering and safe outputs are what protect you here. A workflow that can only add labels from a short list can be tricked into adding the wrong label, which is annoying but not much worse. Pushing to branches is a much bigger deal, so be careful what you give a workflow that reads public input.

By default, only people with admin, maintainer or write access can trigger a workflow, and pull requests from forks are blocked. If you're triggering on pull requests, the quick start recommends pull_request over pull_request_target, since the latter runs with the base repo's permissions and secrets. GitHub is also blocking pull_request_target by default for public repos from 2 December, so it's worth checking any existing workflows that use it.

Engines

If you don't set an engine, you get Copilot. You can authenticate it with a COPILOT_GITHUB_TOKEN secret (a fine-grained personal access token with the Copilot Requests permission), or, if your org pays for Copilot centrally, by adding copilot-requests: write to the workflow's permissions so it uses the built-in Actions token.

To use Claude or Codex instead, set the engine and add your own API key as a secret:

Text
---
engine:
  id: claude
  model: sonnet
---
Text
gh aw secrets set ANTHROPIC_API_KEY --value "your-key"

For Codex, it's engine: codex with an OPENAI_API_KEY (or CODEX_API_KEY) secret. Claude and Codex also have keyless options using OIDC, so you don't need a long-lived key sitting in your secrets, and gh aw secrets bootstrap will look through your workflows and ask for any secrets that are missing.

GA also added model routing, where a router picks a model based on the type of task and how hard it is. I'd start with a single model you know, though, so you can tell whether a bad run was down to the prompt or the model.

What it costs

There are two costs. First, there are your Actions minutes, at standard Actions pricing. The agent job usually takes anywhere from one to 15 minutes, plus about a minute and a half of runner setup for each job.

Then there's the model itself. If you're on Copilot, that comes out of your Copilot plan, and with your own key, it's on your Anthropic or OpenAI bill.

gh-aw tracks this in AI Credits (1 AIC is $0.01), estimated from public model pricing, so treat it as a rough guide rather than your actual bill.

The detection job is a second AI run every time, with its own budget (400 AIC by default), so it adds to the cost too.

Limits you can set

A few limits are on by default, and you can tighten them in the frontmatter:

  • timeout-minutes: 20 minutes for the agent step
  • max-ai-credits: 1,000 AIC (about $10) per run
  • max-daily-ai-credits: 5,000 AIC per workflow per day (once it's hit, the agent is skipped)
  • max-turns: 500 agent turns For something like the stale PR summary, I'd set max-ai-credits much lower than the default. If the agent gets stuck going round in circles, you'd much rather it stopped after a dollar or two than ten.

Forecasting

Once a workflow has run a few times, gh aw forecast uses its run history to estimate what it'll cost over a week or a month:

Text
gh aw forecast stale-prs

It projects a month by default (add --period week for a week), and gives you a low, typical and high estimate (P10, P50 and P90), since agent runs vary a fair bit. Since GA, it includes the threat detection credits too. It's worth running this after your staged runs and before you leave a schedule running, especially for anything triggered by events, where you don't control how often it runs.

The cost docs also suggest starting with schedule or workflow_dispatch triggers, and avoiding push triggers in busy repos, since every push would kick off an agent.

When not to use it

Agentic workflows are great when a job needs a bit of judgement. But plenty of jobs don't, and for those an agent just makes things slower, more expensive and less predictable.

Anything with a fixed right answer

Builds, tests, linting, releases and deploys belong in plain Actions. GitHub says as much: agentic workflows sit alongside your CI, they don't replace it. If you can write the steps down as a script, write the script.

Anything you need to behave the same every time

The same workflow can do something slightly different on each run. That's fine for a summary or a suggested label, but it's not what you want for something like a version bump or a changelog that has to follow a format.

Writes nobody will review

If a workflow is allowed to change things and nobody reads what it's done, a bad run can go unnoticed for weeks. Only give a workflow write access to things somebody actually looks at.

Wrapping up

There's a lot here, but you don't need all of it on day one. I'd start with one read-only workflow that can only open a single issue, set staged: true, and run it by hand a few times while you read what it would have done.

Once you're happy with it, check the forecast, remove staged, and let it run on its schedule. Only after you've watched it behave for a while would I give it anything more, like labels, comments or pull requests.