# Run AI prompts in your GitHub Actions with the Vercel AI Action

**Author:** Ben Sabic

---

You can call the [AI Gateway](https://vercel.com/ai-gateway) directly from a GitHub Actions workflow step with `vercel/ai-action@v2`, generating text or structured JSON without writing a custom script or maintaining provider SDKs. You pass a prompt, pick a model, and read the result from the step's outputs as `text` or, when you provide a JSON Schema, as a parsed `json` object. Use it to triage issues, draft release notes, moderate comments, or run a lightweight code review on pull requests.

This guide walks you through running a basic text prompt, steering the model with a system message, and generating structured JSON that conforms to a schema. You'll also learn how to handle untrusted user content like issue bodies and PR descriptions safely, since prompt injection is a real risk when an AI step's output flows into a `run:` block.

## Prerequisites

Before you begin, make sure you have:

- A Vercel account with AI Gateway access
  
- A GitHub repository where you can add secrets and workflows
  
- Permission to edit `.github/workflows/` in that repository
  

## How it works

The action wraps the [AI SDK](https://ai-sdk.dev/) and routes every request through the AI Gateway, so a single `api-key` gives you access to models from 40+ providers. When you pass a `schema`, the action calls `generateObject` and returns a parsed `json` output. Without a schema, it calls `generateText` and returns the response as `text`. Either way, the result is available to later steps through `steps.<id>.outputs.text` or `steps.<id>.outputs.json`.

## Steps

### 1\. Create an AI Gateway API key

Open the [AI Gateway API Keys page](https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%2Fapi-keys) in your Vercel dashboard and click **Create API Key**. Copy the key to your clipboard before closing the dialog.

### 2\. Add the key as a GitHub secret

In your GitHub repository, navigate to **Settings** > **Secrets and variables** > **Actions**, then click **New repository secret**. Name the secret `AI_GATEWAY_API_KEY` and paste the key value.

### 3\. Add a basic workflow

Create `.github/workflows/ai.yml` with a single `vercel/ai-action@v2` step. This workflow runs on every push to `main` and prints the model's response:

`name: Basic AI prompt on: push: branches: - main jobs: generate-text: runs-on: ubuntu-latest steps: - uses: vercel/ai-action@v2 id: prompt with: prompt: 'Why is the sky blue?' model: 'openai/gpt-5.5' api-key: ${{ secrets.AI_GATEWAY_API_KEY }} - run: echo "$TEXT" env: TEXT: ${{ steps.prompt.outputs.text }}`

Browse the full model list on the [models directory](https://vercel.com/ai-gateway/models) and replace `openai/gpt-5.5` with any supported identifier.

### 4\. Add a system message to steer the model

The `system` input sets the model's role and tone. Use it when you want consistent output style across runs, such as for moderation or classification tasks:

`- uses: vercel/ai-action@v2 id: prompt with: system: 'You are a release-notes editor. Reply with one short sentence in past tense.' prompt: 'Summarize this commit: Fix race condition in token refresh logic.' model: 'openai/gpt-5.5' api-key: ${{ secrets.AI_GATEWAY_API_KEY }}`

### 5\. Generate structured JSON with a schema

Pass a JSON Schema in the `schema` input to get a parsed object on `steps.<id>.outputs.json`. The action calls `generateObject`, which constrains the model to produce output matching your schema. This example classifies a GitHub issue:

`- uses: vercel/ai-action@v2 id: classify with: model: 'openai/gpt-5.5' api-key: ${{ secrets.AI_GATEWAY_API_KEY }} system: 'You are a triage assistant. Classify GitHub issues by type and urgency.' prompt: | Issue Title: ${{ github.event.issue.title }} Issue Body: ${{ github.event.issue.body }} schema: | { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "type": { "type": "string", "enum": ["bug", "feature", "question", "other"] }, "urgent": { "type": "boolean", "description": "True if the issue blocks the user from a critical task." }, "reason": { "type": "string", "description": "One-sentence explanation of the classification." } }, "required": ["type", "urgent"], "additionalProperties": false }` Read fields from the JSON output with `fromJSON`: `- if: fromJSON(steps.classify.outputs.json).urgent run: gh issue edit "$NUMBER" --add-label urgent env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} NUMBER: ${{ github.event.issue.number }}` **Note:** The example above shows the safe pattern for using AI output in shell commands. See the next section for why this matters. ## Handle untrusted input safely Issue titles, issue bodies, PR descriptions, and comments are user input. Treating them as trusted in a `run:` block exposes your workflow to prompt injection. An attacker can craft content that manipulates the model into producing shell commands, which then execute with your workflow's permissions. ### Why string interpolation in `run:` is dangerous GitHub Actions performs variable interpolation in `run:` blocks before the shell sees the script. If you write `echo "${{ steps.classify.outputs.json }}"` and the model returns text containing backticks or `$()`, the shell evaluates those substitutions. An attacker who controls the prompt input can therefore execute arbitrary commands inside your runner. ### The safe pattern: use environment variables Pass any value derived from user input or model output through the `env:` block, then reference it as a shell variable. The shell receives the value as a literal string and never re-evaluates it: `- name: Apply label if: fromJSON(steps.classify.outputs.json).urgent run: | gh issue edit "$NUMBER" --add-label urgent echo "Reason: $REASON" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} NUMBER: ${{ github.event.issue.number }} REASON: ${{ fromJSON(steps.classify.outputs.json).reason }}` ### Rules of thumb Apply these rules whenever a workflow handles user-controlled content or AI output: - **Never interpolate** `**${{ ... }}**` **expressions directly inside a** `**run:**` **script.** Move every dynamic value into `env:` first.    - **Quote shell variables.** Use `"$VAR"` rather than `$VAR` so values with spaces or special characters don't break the command.    - **Scope tokens to the minimum.** Use `GITHUB_TOKEN` with the least privilege needed instead of a long-lived personal access token.    - **Treat model output as user input.** Even if your prompt comes from a maintainer, the model's response is influenced by anything the model has read, including issue bodies it was asked to summarize.    ## Inputs and outputs reference The action accepts the following inputs: | Input     | Required | Description                                                                                                         | | --------- | -------- | ------------------------------------------------------------------------------------------------------------------- | | `prompt`  | Yes      | The input prompt sent to the model.                                                                                 | | `api-key` | Yes      | Your AI Gateway API key. Always pass this from `secrets`, never inline.                                             | | `model`   | Yes      | A model identifier from the [models directory](https://vercel.com/ai-gateway/models), for example `openai/gpt-5.5`. |
| `system`  | No       | A system message that sets the model's role or instructions.                                                        |
| `schema`  | No       | A JSON Schema (draft 2020-12 or compatible). When set, the action returns structured JSON instead of plain text.    |

The action exposes these outputs on `steps.<id>.outputs`:

| Output | Available when  | Description                                                                     |
| ------ | --------------- | ------------------------------------------------------------------------------- |
| `text` | Always          | The model's response. When `schema` is set, this contains the JSON as a string. |
| `json` | `schema` is set | The parsed object that conforms to your schema.                                 |

## Common use cases

These patterns combine the basic steps above into complete workflows.

### Triage new issues

Classify incoming issues and label the urgent ones. Use a schema so you can branch on a boolean rather than parsing free text.

### Summarize pull requests for release notes

Run the action on `pull_request: closed` events with `merged == true`, ask for a one-line summary, and append the output to a `RELEASE_`[`NOTES.md`](http://NOTES.md) file in a follow-up commit.

### Light PR code review

Pass the PR diff (via `gh pr diff`) as the prompt and ask the model to flag obvious issues. Post the result as a comment with `gh pr comment`. Keep this advisory only. It isn't a replacement for human review.

### Detect duplicate issues

When a new issue opens, fetch the titles of recent open issues and ask the model to identify likely duplicates. Use a schema that returns the issue numbers it considers related.

## Troubleshooting

### The action fails with an authentication error

Check that the `AI_GATEWAY_API_KEY` secret exists in the correct scope (repository, environment, or organization) and that the workflow references it as `${{` [`secrets.AI`](http://secrets.AI)`_GATEWAY_API_KEY }}`. If the secret was rotated in the Vercel dashboard, update it in GitHub as well.

### The action fails or returns an empty `json` output

When `schema` is set, the action uses `generateObject` to constrain the model's output to your schema. If the model can't produce conforming output, the step fails or the `json` output is missing. Simplify the schema, add `description` fields to guide the model toward the right structure, or switch to a stronger model. Models vary widely in how reliably they follow JSON Schema, so a model swap is often the fastest fix.

### Shell variables contain unexpected characters

This usually indicates that a `${{ ... }}` expression was interpolated directly into a `run:` block. Move the value to `env:` and reference it as a shell variable. Review the [Handle untrusted input safely](https://claude.ai/chat/47ed45cf-df09-4916-b368-bd07d755e603#handle-untrusted-input-safely) section above.

## Related resources

- [AI Gateway](https://vercel.com/docs/ai-gateway)
  
- [AI Gateway model catalog](https://vercel.com/ai-gateway/models)
  
- [AI Gateway authentication](https://vercel.com/docs/ai-gateway/authentication)
  
- [AI SDK](https://ai-sdk.dev/docs/reference/ai-sdk-core/generate-text) [`generateText`](https://ai-sdk.dev/docs/reference/ai-sdk-core/generate-text)
  
- [AI SDK](https://ai-sdk.dev/docs/reference/ai-sdk-core/generate-object) [`generateObject`](https://ai-sdk.dev/docs/reference/ai-sdk-core/generate-object)
  
- [`vercel/ai-action`](https://github.com/vercel/ai-action) [source and examples](https://github.com/vercel/ai-action)
  
- [Security hardening for GitHub Actions](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions)

---

[View full KB sitemap](/kb/sitemap.md)
