Skip to content

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

Use the vercel/ai-action@v2 GitHub Action to call the AI Gateway from a workflow. Generate text or structured JSON, set system messages, and handle untrusted input safely.

6 min read
Last updated May 25, 2026

You can call the 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 see 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.

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

The action wraps the AI SDK 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.

Open the AI Gateway API Keys page in your Vercel dashboard and click Create API Key. Copy the key to your clipboard before closing the dialog.

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.

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:

.github/workflows/ai.yml
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 and replace openai/gpt-5.5 with any supported identifier.

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:

.github/workflows/ai.yml
- 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 }}

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:

.github/workflows/ai.yml
- 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:

.github/workflows/ai.yml
- 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.

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.

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.

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:

.github/workflows/ai.yml
- 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 }}

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.

The action accepts the following inputs:

InputRequiredDescription
promptYesThe input prompt sent to the model.
api-keyYesYour AI Gateway API key. Always pass this from secrets, never inline.
modelYesA model identifier from the models directory, for example openai/gpt-5.5.
systemNoA system message that sets the model's role or instructions.
schemaNoA 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:

OutputAvailable whenDescription
textAlwaysThe model's response. When schema is set, this contains the JSON as a string.
jsonschema is setThe parsed object that conforms to your schema.

These patterns combine the basic steps above into complete workflows.

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

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 file in a follow-up commit.

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.

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.

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_GATEWAY_API_KEY }}. If the secret was rotated in the Vercel dashboard, update it in GitHub as well.

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.

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 section above.

Was this helpful?

supported.