# actions/github-script: the workflow step for API calls that do not fit a first class action

> github-script takes a JavaScript function body as an input and injects an authenticated Octokit client plus the toolkit packages. Version 9 changed how scripts obtain additional clients, and the repository is explicitly closed to contributions.

**actions/github-script** — Write workflows scripting the GitHub API in JavaScript

- Repository: https://github.com/actions/github-script
- Stars: 5,033 · Forks: 587
- Language: TypeScript
- License: MIT
- Published: 2026-10-06 · Updated: 2026-10-06 · Language: en
- Canonical page: https://hysenlabs.com/projects/actions-github-script

## A script input, not a composite action, and the difference matters

The README's description of the action is short enough to quote in spirit: you provide an input named `script` containing the body of an asynchronous JavaScript function call. Because it is a function body rather than a module, the values it needs are already defined when it runs, and the README is explicit that you do not have to import them.

The injected names are the whole API surface. You get `github`, a pre-authenticated octokit/rest.js client with pagination plugins, and `context`, an object holding the workflow run context. You also get `core`, `glob`, `io` and `exec`, which are references to the corresponding `@actions` toolkit packages, and `getOctokit`, a factory for creating additional authenticated clients.

```javascript
github
context
core
glob
io
exec
getOctokit
require
```

That `require` deserves a note, because it is not Node's `require`. It is a proxy wrapper that enables requiring relative paths relative to the current working directory and requiring npm packages installed in that working directory, which is what lets a workflow step reach into a repository's own `node_modules`. There is an escape hatch named `__original_require__` for when you need the unwrapped function.

The design choice here is the difference between this and a composite action. A composite action has `steps:` in a YAML file and each step is a process. This is one step running one function with the toolkit preloaded. For a single API call that is the right weight. For anything that needs its own retry policy, its own tests, or a place to put a helper function, the script starts to be a program living in a YAML string, which is where this approach stops scaling.

## Version 9 removed require and made getOctokit a parameter

v9.0.0 was published on 2026-04-09 and is the current major. Its headline change is the upgrade to `@actions/github` v9, which brings the latest Octokit types and features, and the consequences of that upgrade are what a migrating user will actually hit.

The first is that `require('@actions/github')` no longer works in scripts, because v9 of that package is ESM-only. The README gives the exact pattern that breaks, `const { getOctokit } = require('@actions/github')`, and the exact replacement: the new injected `getOctokit` function, available directly with no imports.

The second is subtler and produces a worse error message. Since `getOctokit` is now an injected function parameter, a script that declares `const getOctokit = ...` or `let getOctokit = ...` gets a SyntaxError, because JavaScript does not allow redeclaring a parameter. The README's advice is to use the injected one directly, or to write `var getOctokit = ...` if you genuinely need to redeclare it. A script that shadows the name to build a custom client will fail at parse time rather than at the point where you would expect.

There is also a new behaviour that is not breaking but is worth knowing. The `ACTIONS_ORCHESTRATION_ID` environment variable is now appended to the user-agent string automatically for request tracing, so a request from a workflow can be tied back to the run that made it. That matters when you are looking at GitHub-side logs and wondering which of several concurrent runs issued a call.

The README also notes that scripts reaching into `@actions/github` internals beyond the standard client may need updating for v9 compatibility, which is the usual residual risk in a major that moves a library to ESM.

## The long history of Node runtime bumps is the real upgrade cost

The README's Breaking Changes section runs from v9 down to v5, and four of the five entries are about the Node runtime rather than about the action's own API. v6 moved from Node 12 to Node 16, v7 moved from Node 16 to Node 20, and v8 moved from Node 20 to Node 24. Each says scripts are now affected by any breaking changes between those Node versions, which is the honest framing: you are not only upgrading an action, you are upgrading the language runtime your script executes on.

v8 additionally requires a minimum Actions Runner version of v2.327.1. That number is the practical constraint for self-hosted runners, and it is the same floor that actions/cache v5 requires.

```json
  "engines": {
    "node": ">=24"
  },
```

The v5 entry is the one that catches people out years later, because it changed the shape of every call rather than the runtime. From v5 onward the Octokit context available via `github` no longer has REST methods directly; they live under `github.rest.*`. The README's example is precise: `github.issues.createComment` in v4 becomes `github.rest.issues.createComment` in v5, while `github.request`, `github.paginate` and `github.graphql` are unchanged. That asymmetry is why old tutorials and Stack Overflow answers still produce confusing errors.

There is a smaller v7 change that reads like trivia and is not. The `previews` input now applies only to GraphQL API calls, because REST API previews were no longer necessary following the promotion of preview endpoints to GA. If you were passing previews to make a REST call work, you can delete that.

The pattern across five majors is that each one is a runtime or library major, not a redesign. Scripts that only use `github.rest.*` and `core` have been stable since v5; everything else has been paying for the underlying stack.

## The repository is closed to contributions, and says why

The Note section near the top of the README is unusual enough to be worth reading in full. It says the project is not taking contributions, that resources are being allocated towards other areas of Actions, and that the GitHub public roadmap is the place to follow for updates. It then redirects where questions go: support requests to a Community Discussions area, high priority bugs to Community Discussions or GitHub support, and security issues through the security policy.

The commitment it does make is narrow and specific: security updates will still be provided, and major breaking changes will be fixed. That is a coherent position for an action maintained inside a large platform, and it is also a decision an adopter has to plan around.

The build setup reflects a mature internal project. `package.json` names the package `@actions/github-script` at version 9.0.0, requires Node 24 or newer, points `main` at `dist/index.js` and `types` at `types/async-function.d.ts`, and builds with ncc from `src/main.ts` after generating declarations with tsc. Runtime dependencies are `@actions/core`, `@actions/exec`, `@actions/github` v9, `@actions/glob`, `@actions/io`, and on the Octokit side `@octokit/core` v7 with the request-log and retry plugins.

```json
    "@actions/github": "^9.0.0",
    "@actions/glob": "^0.4.0",
    "@octokit/core": "^7.0.0",
```

The tree has `src/`, `dist/`, `__test__/` with a single underscore where actions/cache has two, `docs/`, `types/`, an `action.yml`, a `CODEOWNERS` file, and a `.husky/` directory wired to a `prepare` script, so commits run style checks, tests and a build before landing. There are three workflow badges, for integration, CI and licensed.

## When this is the wrong tool

The strongest argument against reaching for this action is that the code lives in YAML. A script with a loop, a retry, and an error path is a program, and a program in a workflow file gets no linting, no unit tests, no review diff that shows you what changed in the logic, and no reusable entry point for a second workflow. The repository's own `__test__/` directory tests the action, not the scripts people write with it.

The second problem is the escape hatches. When `require` is proxied so that relative paths resolve from the working directory, and when `exec` and `io` are both injected, a script can quietly do a lot: run commands, move files, install things. That is genuinely useful and it is also the reason the trusted-publishing model for actions matters here more than for an action that only makes one API call.

A reasonable rule of thumb follows from that. If the step is one API call plus maybe a `core.setFailed`, github-script is a good fit and beats shelling out with `gh api`. If the step has branching logic, retries, or needs to be called from three workflows, write the script into the repository, run it with a normal `run:` step or a setup-node step, and use github-script only for the API calls.

Comparing to the alternatives: gh and the REST API via curl give you the same capability with none of the injected conveniences and considerably worse ergonomics around pagination, context and error reporting. A custom action in TypeScript gives you the most structure and the most maintenance, since you own the build, the node_modules and the runner compatibility. github-script sits deliberately between the two, and the design is at its best when you accept that trade.

## Conclusion

github-script earns its place when the API call you need is a one-off: comment on an issue, read a workflow run, set a label, flip something on an external service through `exec`. It is the wrong tool for a multi-step job with retries and its own error handling, which belongs in a composite action or a checked-in script you can lint and unit test. The v9 migration is the one thing to do before anything else, because `require('@actions/github')` now fails at runtime and a script that redeclares `getOctokit` throws a SyntaxError from parameter shadowing rather than anything that names the cause. The repository states plainly that it is not taking contributions and will only ship security updates and major breaking fixes, with the last push on 2026-04-09, so plan on pinning a major version rather than expecting features.

## FAQ

### How do I run a GitHub script?

For this action, provide an input named `script` whose value is the body of an asynchronous JavaScript function call. The action injects `github`, a pre-authenticated octokit/rest.js client with pagination plugins, `context` for the workflow run, and references to the `core`, `glob`, `io` and `exec` toolkit packages. Since the script is a function body rather than a module, none of those need importing.

### What changed in actions/github-script v9?

It upgrades to `@actions/github` v9, which is ESM-only. As a result `require('@actions/github')` fails at runtime, so scripts that built a second client that way must use the injected `getOctokit` instead. Because `getOctokit` is now a function parameter, redeclaring it with `const` or `let` produces a SyntaxError; use `var` if you need to. The release also appends `ACTIONS_ORCHESTRATION_ID` to the user-agent for request tracing.

### Why does github.issues.createComment no longer work?

From version 5 onward the Octokit context available via `github` no longer exposes REST methods directly. They moved under `github.rest.*`, so `github.issues.createComment` becomes `github.rest.issues.createComment`. `github.request`, `github.paginate` and `github.graphql` are unchanged by that move, which is why some calls in an old script keep working while others fail.

### What Node version and runner does actions/github-script v9 need?

Node 24, since version 8 moved the runtime from Node 20 to Node 24 and `package.json` requires node 24 or newer. Version 8 also set the minimum Actions Runner version at v2.327.1, which matters most for self-hosted runners. Each of v6, v7 and v8 was a runtime bump, and the README notes that scripts are affected by breaking changes between the Node versions involved.

### Can I still contribute to actions/github-script?

No. The README states the project is not taking contributions and that resources are being directed to other areas of Actions. Support requests go to the Community Discussions area, high priority bugs can go there or to GitHub support, and security issues follow the security policy. The commitment that remains is security updates and fixes for major breaking changes.

## Sources

- [actions/github-script on GitHub](https://github.com/actions/github-script)
- [Issues](https://github.com/actions/github-script/issues)
- [License: MIT](https://github.com/actions/github-script/blob/main/LICENSE)
- [README](https://github.com/actions/github-script/blob/main/README.md)
- [Releases](https://github.com/actions/github-script/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/actions-github-script
