# What is StarSling? (/)
## Introduction
Developers typically spend 20-30% of their time on annoying eng tasks outside of their code editor:
* Fixing exceptions
* Patching failed builds and flaky tests
* Investigating and remediating incidents
* Optimizing database and application performance
These tasks are spread out across many different developer tools and workflows. StarSling is unifying these tools and bringing AI agents to the rest of the software development lifecycle (SDLC), starting with continuous integration (CI). Learn more at [starsling.dev](https://starsling.dev).
## StarSling Runners: self-driving CI
StarSling Runners are an AI-native drop-in replacement for `ubuntu-latest` and `ubuntu-24.04` that make your GitHub Actions up to 6x faster. StarSling agents do continuous deep scans of your CI setup and ship optimizations to keep speeding up your builds and save you minutes.
## Quick Start
2,000 free minutes for your first month — no credit card required.
### Step 1: Install the StarSling GitHub App
[Install StarSling GitHub App](https://github.com/apps/starslingdev)
StarSling Runners are not available for personal repositories, only GitHub organizations. If you install the GitHub App in a personal repo, StarSling Runners will not pick up the jobs. [Learn why →](https://docs.starsling.dev/troubleshooting/common-issues#personal-repositories-not-supported)
### Step 2: Update Your Workflow(s)
Paste this prompt into any AI coding agent (Claude Code, Cursor, Codex, etc.):
````markdown title="AI prompt"
# Migrate GitHub Actions to StarSling Runners
Migrate the user's workflows from GitHub-hosted runners to StarSling Runners.
**Prerequisites:** `gh` CLI authenticated, [StarSling GitHub App](https://github.com/apps/starslingdev) installed on the repo's org.
## Configuration
**Target:** `starsling-ubuntu-24.04` | **Branch:** `migrate-starsling-ubuntu-2404`
**Source runners to replace:** `ubuntu-latest`, `ubuntu-24.04`
Replace all UPPERCASE placeholders (`OWNER`, `REPO`, `BRANCH_NAME`, `HEAD_OID`, `BASE64_CONTENT`, `FILE_NAME`, `N`, `DEFAULT_BRANCH`) with actual values from previous steps.
## Procedure
### Step 1: Confirm GitHub App Installation
Ask the user: "Have you installed the [StarSling GitHub App](https://github.com/apps/starslingdev) on your org? It's required for runners to pick up jobs after merge. If not, please install it first and let me know when you're ready."
**Do not run any commands or proceed to Step 2 until the user explicitly confirms the app is installed.**
### Step 2: Verify CLI Auth
Verify `gh auth status` succeeds. If not, direct the user to install from https://cli.github.com/ and run `gh auth login`.
### Step 3: Get Repository
Ask the user for the repository (`owner/repo`). Then run `gh api repos/OWNER/REPO --jq '.owner.type'`. If the result is `User` (not `Organization`), stop and explain: "StarSling Runners only work with GitHub organization repositories. You can create a free organization at https://github.com/account/organizations/new."
### Step 4: Discover Workflows
Fetch all workflow files in one API call:
```bash
cat <<'QUERY' | gh api graphql --input -
{
"query": "query($owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { id nameWithOwner defaultBranchRef { name target { oid } } object(expression: \"HEAD:.github/workflows\") { ... on Tree { entries { name object { ... on Blob { text } } } } } } }",
"variables": { "owner": "OWNER", "repo": "REPO" }
}
QUERY
```
Note: `HEAD` in the GraphQL expression is a Git ref, not a placeholder — do not replace it.
If the response is truncated or errors due to size, fall back to the REST API: fetch the default branch and HEAD SHA via `gh api repos/OWNER/REPO --jq '.default_branch'` and `gh api repos/OWNER/REPO/git/ref/heads/DEFAULT_BRANCH --jq '.object.sha'`, then fetch workflow file names via `gh api repos/OWNER/REPO/contents/.github/workflows` and each file individually via `gh api repos/OWNER/REPO/contents/.github/workflows/FILE_NAME`. The response `content` field is base64-encoded — decode it before scanning for `runs-on:` values.
Save `defaultBranchRef.name` (or REST default branch), `.target.oid` / HEAD SHA, and workflow `entries[]`. Scan each `.yml`/`.yaml` file for:
- direct `runs-on:` string values matching supported runners, and
- matrix-driven patterns (e.g., `runs-on: ${{ matrix.os }}`) where the matrix values include supported runner labels.
Show the user a summary including:
- Workflows that will be migrated and which runners are being replaced
- Any Ubuntu runners NOT in the supported list (e.g., `ubuntu-22.04`, `ubuntu-20.04`, larger runners like `ubuntu-latest-16-cores`) listed as "Not migrated — unsupported runner label"
If no direct or matrix-backed supported runners match, stop.
### Step 5: Preview Changes
**Never create a PR without the user confirming changes first.**
Show what will change per workflow:
- Replace supported runners in `runs-on:` string values with `starsling-ubuntu-24.04` (preserve quotes/comments)
- Replace matching `runner:`/`os:` values in `matrix.include` sections
- Skip commented lines
**Complex `runs-on` patterns:**
- **Array syntax** (e.g., `runs-on: [self-hosted, linux, ubuntu-latest]`): Only replace the matching label within the array; do not collapse to a single string
- **Group/labels syntax** (e.g., `runs-on: { group: ..., labels: [...] }`): Skip and flag for manual review
- **Expressions** (`${{ matrix.os }}`, ternary/conditional): If `runs-on` uses `${{ matrix.os }}` and the matrix values are hardcoded runner labels, replace the labels in the matrix definition and flag the workflow for manual verification. If the matrix values come from other expressions, skip entirely and flag for manual review
**YAML fidelity:** Change ONLY the `runs-on` and matrix values. Preserve the original file exactly: same indentation, key ordering, comments, blank lines, and trailing newline. The PR diff should show only the runner label changes.
### Step 6: Create PR
**Branch:** Before creating, check for existing branches: `gh api repos/OWNER/REPO/git/matching-refs/heads/migrate-starsling-ubuntu-2404 --jq '.[].ref'`. If any exist, find the highest numeric suffix and increment by 1 (if none have a suffix, use `-2`). Then create: `gh api repos/OWNER/REPO/git/refs -f ref=refs/heads/BRANCH_NAME -f sha=HEAD_OID`.
**Atomic commit** — all files in one commit, contents base64-encoded:
```bash
cat <<'MUTATION' | gh api graphql --input -
{
"query": "mutation($input: CreateCommitOnBranchInput!) { createCommitOnBranch(input: $input) { commit { oid url } } }",
"variables": {
"input": {
"branch": {
"repositoryNameWithOwner": "OWNER/REPO",
"branchName": "BRANCH_NAME"
},
"message": {
"headline": "Migrate N CI workflows to StarSling Runners",
"body": "Replaced runners per file:\n- file1.yml: ubuntu-latest → starsling-ubuntu-24.04\n- file2.yml: ubuntu-24.04 → starsling-ubuntu-24.04"
},
"expectedHeadOid": "HEAD_OID",
"fileChanges": {
"additions": [
{ "path": ".github/workflows/FILE_NAME", "contents": "BASE64_CONTENT" }
]
}
}
}
}
MUTATION
```
Verify the response contains a valid `commit.oid`. If the mutation returned an `expectedHeadOid` mismatch, re-fetch the HEAD SHA (Step 4) and retry the commit once. For any other errors, report and stop — do not create a PR against a failed commit.
**Commit message:** The headline should read `Migrate N CI workflows to StarSling Runners` where N is the count of modified workflow files (`.yml` + `.yaml`). List each file and the runner label(s) replaced in the body, as shown in the example above.
**PR** via `gh pr create --repo OWNER/REPO --head BRANCH_NAME --base DEFAULT_BRANCH --title "Migrate N CI workflows to StarSling Runners" --body "..."` with this body structure:
```
## Summary
Migrates CI workflows from GitHub-hosted runners to [StarSling Runners](https://docs.starsling.dev) for faster builds and AI-powered optimizations.
## Changes
- `file1.yml`: `ubuntu-latest` → `starsling-ubuntu-24.04`
- `file2.yml`: `ubuntu-24.04` → `starsling-ubuntu-24.04`
## Not Migrated
- `file3.yml`: Uses `${{ matrix.os }}` — requires manual review
(or "All workflows migrated successfully.")
## After Merging
Workflows will automatically run on StarSling Runners. Ensure the [StarSling GitHub App](https://github.com/apps/starslingdev) is installed with access to this repo.
```
### Step 7: Monitor (Optional)
Ask the user if they'd like help monitoring after merge. If yes, explain they can return after merging and you'll check with:
```bash
gh run list --repo OWNER/REPO --branch DEFAULT_BRANCH --limit 5 --json status,conclusion,name,createdAt
```
Look for runs created after the merge. If any show `queued` for more than 2 minutes, suggest checking the GitHub App installation and repo access at https://github.com/apps/starslingdev.
## Error Handling
| Error | Solution |
|-------|----------|
| `gh` not found or not logged in | Install from https://cli.github.com/, run `gh auth login` |
| 403 / insufficient permissions | User needs write access to the repository — check collaborator status or org role |
| App not installed | Install from https://github.com/apps/starslingdev |
| Personal repo (owner type `User`) | StarSling requires an org repo — create one at https://github.com/account/organizations/new |
| Repository not found | Check repo name and permissions |
| No workflows found | Ensure `.github/workflows/` exists |
| Runner not available after merge | Verify app has repo access at https://github.com/apps/starslingdev |
| Rate limit exceeded | Wait a few minutes and retry |
| 422 on branch creation | Branch exists — append a number suffix |
| `expectedHeadOid` mismatch | Re-fetch HEAD SHA and retry the commit |
| `${{ matrix.os }}` / complex `runs-on` | Require manual review — determined at runtime |
| GraphQL response truncated | Fall back to REST API for individual file fetches |
````
See the [full quickstart](https://docs.starsling.dev/getting-started/quickstart) for a manual `runs-on` label swap and other migration options.
## What You Get
### Up to 6x Faster CI Runs
* **[30% faster hardware](https://docs.starsling.dev/runners/compute-sizing)**
* **Unlimited concurrency** to minimize queue times
### AI-Powered Optimizations
* **[Optimizations](https://docs.starsling.dev/ai-agents/optimizations)** — AI agents scan your workflows to continuously improve CI speed ([see real examples](https://starsling.dev/#ai-prs)). For new accounts, AI-powered optimization PRs are only available to customers on paid plans and are not enabled by default.
* **[ci-speedup](https://docs.starsling.dev/skills/ci-speedup)** — Run a free, open-source audit on demand with your own coding agent to find the GitHub Actions check gating your PRs.
* **[ci-score](https://docs.starsling.dev/skills/ci-score)** — Grade your GitHub Actions configuration against CI best practices with the same free, open-source skill set, and get a ranked fix for every gap.
* **[ci-secure](https://docs.starsling.dev/skills/ci-secure)** — Scan your workflows for the ten critical CI/CD attack vectors, each reported with the attack it enables and a fix you can apply.
Teams running StarSling see this compound in production:
### Up to 33% Lower Cost
Every StarSling runner is cheaper than GitHub's equivalent larger runner — up to 33%.
| Runner | vCPU | Memory | GitHub Price | StarSling Price | Savings |
| --------------------------- | ---- | ------ | ------------ | --------------- | ------- |
| `starsling-ubuntu-24.04-2` | 2 | 8 GB | $0.006/min | $0.004/min | **33%** |
| `starsling-ubuntu-24.04` | 4 | 16 GB | $0.012/min | **$0.008/min** | **33%** |
| `starsling-ubuntu-24.04-8` | 8 | 32 GB | $0.022/min | $0.016/min | **27%** |
| `starsling-ubuntu-24.04-16` | 16 | 64 GB | $0.042/min | $0.032/min | **24%** |
| `starsling-ubuntu-24.04-32` | 32 | 128 GB | $0.082/min | $0.064/min | **22%** |
| `starsling-ubuntu-24.04-64` | 64 | 256 GB | $0.162/min | $0.128/min | **21%** |
*GitHub prices reference [GitHub Actions runner pricing](https://docs.github.com/en/billing/reference/actions-runner-pricing) for Linux x64 larger runners (as of 2026-05-06).*
**2,000 free minutes for your first month** — no credit card required to start.
## Comparison to GitHub
| Feature | GitHub-hosted | StarSling |
| ------------------------- | ------------- | ------------------- |
| Queue time | 30-60s | **Under 30s (P50)** |
| Build speed | Baseline | **Up to 6x faster** |
| Optimization improvements | Manual | **AI-powered** |
## Get Started
# Quickstart (/getting-started/quickstart)
2,000 free minutes for your first month — no credit card required.
## Step 1: Install the StarSling GitHub App
Install the StarSling GitHub App in your organization:
[Install StarSling GitHub App](https://github.com/apps/starslingdev)
Grant access to the org repositories where you want to use StarSling Runners.
StarSling Runners are not available for personal repositories, only GitHub organizations. If you install the GitHub App in a personal repo, StarSling Runners will not pick up the jobs. [Learn why →](https://docs.starsling.dev/troubleshooting/common-issues#personal-repositories-not-supported)
## Step 2: Update Your Workflow(s)
### Option A: Use an AI Prompt
Paste this prompt into any AI coding agent (Claude Code, Cursor, Codex, etc.):
````markdown title="AI prompt"
# Migrate GitHub Actions to StarSling Runners
Migrate the user's workflows from GitHub-hosted runners to StarSling Runners.
**Prerequisites:** `gh` CLI authenticated, [StarSling GitHub App](https://github.com/apps/starslingdev) installed on the repo's org.
## Configuration
**Target:** `starsling-ubuntu-24.04` | **Branch:** `migrate-starsling-ubuntu-2404`
**Source runners to replace:** `ubuntu-latest`, `ubuntu-24.04`
Replace all UPPERCASE placeholders (`OWNER`, `REPO`, `BRANCH_NAME`, `HEAD_OID`, `BASE64_CONTENT`, `FILE_NAME`, `N`, `DEFAULT_BRANCH`) with actual values from previous steps.
## Procedure
### Step 1: Confirm GitHub App Installation
Ask the user: "Have you installed the [StarSling GitHub App](https://github.com/apps/starslingdev) on your org? It's required for runners to pick up jobs after merge. If not, please install it first and let me know when you're ready."
**Do not run any commands or proceed to Step 2 until the user explicitly confirms the app is installed.**
### Step 2: Verify CLI Auth
Verify `gh auth status` succeeds. If not, direct the user to install from https://cli.github.com/ and run `gh auth login`.
### Step 3: Get Repository
Ask the user for the repository (`owner/repo`). Then run `gh api repos/OWNER/REPO --jq '.owner.type'`. If the result is `User` (not `Organization`), stop and explain: "StarSling Runners only work with GitHub organization repositories. You can create a free organization at https://github.com/account/organizations/new."
### Step 4: Discover Workflows
Fetch all workflow files in one API call:
```bash
cat <<'QUERY' | gh api graphql --input -
{
"query": "query($owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { id nameWithOwner defaultBranchRef { name target { oid } } object(expression: \"HEAD:.github/workflows\") { ... on Tree { entries { name object { ... on Blob { text } } } } } } }",
"variables": { "owner": "OWNER", "repo": "REPO" }
}
QUERY
```
Note: `HEAD` in the GraphQL expression is a Git ref, not a placeholder — do not replace it.
If the response is truncated or errors due to size, fall back to the REST API: fetch the default branch and HEAD SHA via `gh api repos/OWNER/REPO --jq '.default_branch'` and `gh api repos/OWNER/REPO/git/ref/heads/DEFAULT_BRANCH --jq '.object.sha'`, then fetch workflow file names via `gh api repos/OWNER/REPO/contents/.github/workflows` and each file individually via `gh api repos/OWNER/REPO/contents/.github/workflows/FILE_NAME`. The response `content` field is base64-encoded — decode it before scanning for `runs-on:` values.
Save `defaultBranchRef.name` (or REST default branch), `.target.oid` / HEAD SHA, and workflow `entries[]`. Scan each `.yml`/`.yaml` file for:
- direct `runs-on:` string values matching supported runners, and
- matrix-driven patterns (e.g., `runs-on: ${{ matrix.os }}`) where the matrix values include supported runner labels.
Show the user a summary including:
- Workflows that will be migrated and which runners are being replaced
- Any Ubuntu runners NOT in the supported list (e.g., `ubuntu-22.04`, `ubuntu-20.04`, larger runners like `ubuntu-latest-16-cores`) listed as "Not migrated — unsupported runner label"
If no direct or matrix-backed supported runners match, stop.
### Step 5: Preview Changes
**Never create a PR without the user confirming changes first.**
Show what will change per workflow:
- Replace supported runners in `runs-on:` string values with `starsling-ubuntu-24.04` (preserve quotes/comments)
- Replace matching `runner:`/`os:` values in `matrix.include` sections
- Skip commented lines
**Complex `runs-on` patterns:**
- **Array syntax** (e.g., `runs-on: [self-hosted, linux, ubuntu-latest]`): Only replace the matching label within the array; do not collapse to a single string
- **Group/labels syntax** (e.g., `runs-on: { group: ..., labels: [...] }`): Skip and flag for manual review
- **Expressions** (`${{ matrix.os }}`, ternary/conditional): If `runs-on` uses `${{ matrix.os }}` and the matrix values are hardcoded runner labels, replace the labels in the matrix definition and flag the workflow for manual verification. If the matrix values come from other expressions, skip entirely and flag for manual review
**YAML fidelity:** Change ONLY the `runs-on` and matrix values. Preserve the original file exactly: same indentation, key ordering, comments, blank lines, and trailing newline. The PR diff should show only the runner label changes.
### Step 6: Create PR
**Branch:** Before creating, check for existing branches: `gh api repos/OWNER/REPO/git/matching-refs/heads/migrate-starsling-ubuntu-2404 --jq '.[].ref'`. If any exist, find the highest numeric suffix and increment by 1 (if none have a suffix, use `-2`). Then create: `gh api repos/OWNER/REPO/git/refs -f ref=refs/heads/BRANCH_NAME -f sha=HEAD_OID`.
**Atomic commit** — all files in one commit, contents base64-encoded:
```bash
cat <<'MUTATION' | gh api graphql --input -
{
"query": "mutation($input: CreateCommitOnBranchInput!) { createCommitOnBranch(input: $input) { commit { oid url } } }",
"variables": {
"input": {
"branch": {
"repositoryNameWithOwner": "OWNER/REPO",
"branchName": "BRANCH_NAME"
},
"message": {
"headline": "Migrate N CI workflows to StarSling Runners",
"body": "Replaced runners per file:\n- file1.yml: ubuntu-latest → starsling-ubuntu-24.04\n- file2.yml: ubuntu-24.04 → starsling-ubuntu-24.04"
},
"expectedHeadOid": "HEAD_OID",
"fileChanges": {
"additions": [
{ "path": ".github/workflows/FILE_NAME", "contents": "BASE64_CONTENT" }
]
}
}
}
}
MUTATION
```
Verify the response contains a valid `commit.oid`. If the mutation returned an `expectedHeadOid` mismatch, re-fetch the HEAD SHA (Step 4) and retry the commit once. For any other errors, report and stop — do not create a PR against a failed commit.
**Commit message:** The headline should read `Migrate N CI workflows to StarSling Runners` where N is the count of modified workflow files (`.yml` + `.yaml`). List each file and the runner label(s) replaced in the body, as shown in the example above.
**PR** via `gh pr create --repo OWNER/REPO --head BRANCH_NAME --base DEFAULT_BRANCH --title "Migrate N CI workflows to StarSling Runners" --body "..."` with this body structure:
```
## Summary
Migrates CI workflows from GitHub-hosted runners to [StarSling Runners](https://docs.starsling.dev) for faster builds and AI-powered optimizations.
## Changes
- `file1.yml`: `ubuntu-latest` → `starsling-ubuntu-24.04`
- `file2.yml`: `ubuntu-24.04` → `starsling-ubuntu-24.04`
## Not Migrated
- `file3.yml`: Uses `${{ matrix.os }}` — requires manual review
(or "All workflows migrated successfully.")
## After Merging
Workflows will automatically run on StarSling Runners. Ensure the [StarSling GitHub App](https://github.com/apps/starslingdev) is installed with access to this repo.
```
### Step 7: Monitor (Optional)
Ask the user if they'd like help monitoring after merge. If yes, explain they can return after merging and you'll check with:
```bash
gh run list --repo OWNER/REPO --branch DEFAULT_BRANCH --limit 5 --json status,conclusion,name,createdAt
```
Look for runs created after the merge. If any show `queued` for more than 2 minutes, suggest checking the GitHub App installation and repo access at https://github.com/apps/starslingdev.
## Error Handling
| Error | Solution |
|-------|----------|
| `gh` not found or not logged in | Install from https://cli.github.com/, run `gh auth login` |
| 403 / insufficient permissions | User needs write access to the repository — check collaborator status or org role |
| App not installed | Install from https://github.com/apps/starslingdev |
| Personal repo (owner type `User`) | StarSling requires an org repo — create one at https://github.com/account/organizations/new |
| Repository not found | Check repo name and permissions |
| No workflows found | Ensure `.github/workflows/` exists |
| Runner not available after merge | Verify app has repo access at https://github.com/apps/starslingdev |
| Rate limit exceeded | Wait a few minutes and retry |
| 422 on branch creation | Branch exists — append a number suffix |
| `expectedHeadOid` mismatch | Re-fetch HEAD SHA and retry the commit |
| `${{ matrix.os }}` / complex `runs-on` | Require manual review — determined at runtime |
| GraphQL response truncated | Fall back to REST API for individual file fetches |
````
### Option B: Manual Update
Change your `runs-on` label from GitHub-hosted to StarSling:
```
starsling-ubuntu-24.04
```
### Before
```yaml title=".github/workflows/ci.yml"
jobs:
build:
runs-on: ubuntu-latest # [!code highlight]
steps:
- uses: actions/checkout@v4
# ... your build steps
```
### After
```yaml title=".github/workflows/ci.yml"
jobs:
build:
runs-on: starsling-ubuntu-24.04 # [!code highlight]
steps:
- uses: actions/checkout@v4
# ... your build steps
```
That's it. Push your changes and watch your builds fly.
## Label Mapping
Use `starsling-ubuntu-24.04` (4 vCPU / 16 GB) to replace these GitHub-hosted runner labels:
| GitHub-hosted Label | StarSling Label |
| ------------------- | ------------------------ |
| `ubuntu-latest` | `starsling-ubuntu-24.04` |
| `ubuntu-24.04` | `starsling-ubuntu-24.04` |
StarSling also offers additional runner sizes (2, 8, 16, 32, and 64 vCPU). See [Instance Types](https://docs.starsling.dev/runners/instance-types) for the full label reference.
## What's Next?
# Instance Types (/runners/instance-types)
StarSling Runners are built on Linux Ubuntu with pre-installed tools for common CI/CD workflows.
## Runner Labels
Choose a runner label based on the compute resources your job requires. For non-default sizes, the numeric suffix on each label indicates the vCPU count (e.g. `-8` = 8 vCPU). The label without a suffix is the 4 vCPU default.
| Label | OS | vCPU | Memory | Price per minute |
| --------------------------- | ------------ | ---- | ------ | ---------------- |
| `starsling-ubuntu-24.04-2` | Ubuntu 24.04 | 2 | 8 GB | $0.004 |
| `starsling-ubuntu-24.04` | Ubuntu 24.04 | 4 | 16 GB | $0.008 |
| `starsling-ubuntu-24.04-8` | Ubuntu 24.04 | 8 | 32 GB | $0.016 |
| `starsling-ubuntu-24.04-16` | Ubuntu 24.04 | 16 | 64 GB | $0.032 |
| `starsling-ubuntu-24.04-32` | Ubuntu 24.04 | 32 | 128 GB | $0.064 |
| `starsling-ubuntu-24.04-64` | Ubuntu 24.04 | 64 | 256 GB | $0.128 |
The `starsling-ubuntu-24.04` label (4 vCPU / 16 GB) is the default and replaces the following GitHub-hosted runner labels:
* `ubuntu-latest`
* `ubuntu-24.04`
## Example Usage
```yaml title=".github/workflows/build.yml"
jobs:
build:
runs-on: starsling-ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install and Build
run: |
npm ci
npm run build
```
# Compute Specifications (/runners/compute-sizing)
StarSling offers two runner families: CPU runners on 5th Gen AMD EPYC processors, and GPU runners backed by NVIDIA cards. Choose a runner based on the compute resources your job requires.
## CPU Specifications
| Label | vCPU | Memory | Price per minute |
| --------------------------- | ---- | ------ | ---------------- |
| `starsling-ubuntu-24.04-2` | 2 | 8 GB | $0.004 |
| `starsling-ubuntu-24.04` | 4 | 16 GB | $0.008 |
| `starsling-ubuntu-24.04-8` | 8 | 32 GB | $0.016 |
| `starsling-ubuntu-24.04-16` | 16 | 64 GB | $0.032 |
| `starsling-ubuntu-24.04-32` | 32 | 128 GB | $0.064 |
| `starsling-ubuntu-24.04-64` | 64 | 256 GB | $0.128 |
The numeric suffix on each label indicates the vCPU count (e.g. `-8` = 8 vCPU). The label without a suffix is the 4 vCPU default and replaces `ubuntu-latest` and `ubuntu-24.04`.
All sizes run Ubuntu 24.04 on 5th Gen AMD EPYC.
## GPU Specifications
GPU runners are in **private beta**. Contact [founders@starsling.dev](mailto:founders@starsling.dev) to request access.
| Label | GPU | vCPU | Memory | Disk | Price per minute |
| -------------------------------------------- | ------------ | ---- | ------ | ------ | ---------------- |
| `starsling-ubuntu-24.04-gpu` | RTX PRO 6000 | 4 | 16 GB | 100 GB | $0.05922 |
| `starsling-ubuntu-24.04-gpu/gpus=rtx-5090:1` | RTX 5090 | 4 | 16 GB | 100 GB | $0.03022 |
| `starsling-ubuntu-24.04-gpu/gpus=rtx-4090:1` | RTX 4090 | 4 | 16 GB | 100 GB | $0.02522 |
All cards are NVIDIA. Prices are all-in: each rate covers the GPU and the host it runs on, billed per minute like any other runner.
### Selecting a card
The `gpus=` selector takes a card SKU and a card count, `gpus=:`. While GPU runners are in beta they are single-card, so the count is always `1`.
`starsling-ubuntu-24.04-gpu` is shorthand for the default card — it is equivalent to `starsling-ubuntu-24.04-gpu/gpus=rtx-pro-6000:1`. See the [Label Reference](https://docs.starsling.dev/configuration/label-reference#gpu-labels) for the full label grammar and the list of card SKUs.
## Performance Characteristics
StarSling runners deliver excellent performance for most CI workloads:
* **Web Applications (React, Next.js, Vue)**: Fast installs and builds
* **Backend Services (Go, Rust, Java)**: Efficient parallel compilation
* **Test Suites**: Parallel test execution
Larger sizes (8 vCPU and above) are well-suited for monorepo builds, large test matrices, and compilation-heavy workloads.
## Example Usage
```yaml
runs-on: starsling-ubuntu-24.04
```
For a larger size:
```yaml
runs-on: starsling-ubuntu-24.04-8
```
On a GPU runner:
```yaml
runs-on: starsling-ubuntu-24.04-gpu
```
To pick a specific card:
```yaml
runs-on: starsling-ubuntu-24.04-gpu/gpus=rtx-5090:1
```
## Optimization Tips
1. **Cache dependencies**: Use `actions/cache` to avoid repeated downloads
2. **Use matrix builds**: Split tests across multiple runners for parallelism
3. **Right-size your runner**: Larger isn't always better — measure before scaling up
# Benchmarks (/performance/benchmarks)
StarSling runners can deliver up to 6x faster build times compared to GitHub-hosted runners, depending on workload characteristics. The best way to see the difference is to benchmark your own workflows — the guide below shows how.
## Real-World Results
What StarSling delivered for real teams after they switched `runs-on` — faster runners plus the optimizations StarSling agents shipped and the team merged:
| Repo | Metric | GitHub-hosted | StarSling | Improvement |
| ---------------------------------------------------------- | -------------------- | ------------- | --------- | ---------------- |
| [Mastra](https://starsling.dev/customers/mastra) | Slowest test suite | \~30m | \~5m | **\~6x faster** |
| [Mastra](https://starsling.dev/customers/mastra) | Job queue under load | \~15m | under 2m | **\~8x shorter** |
| [Better Auth](https://starsling.dev/customers/better-auth) | E2E runtime per job | 2m 22s | 1m 04s | **\~2x faster** |
Across all its CI and E2E jobs, Better Auth saves roughly **20,000 CI minutes every month**.
These are live customer results — each links to its case study with the full methodology and the agent-authored PRs behind the numbers.
## Why StarSling is Faster
### 1. 30% Faster Hardware
| Spec | GitHub-hosted | StarSling |
| ------ | ----------------------------- | ---------------- |
| CPU | 4th Gen AMD EPYC\* | 5th Gen AMD EPYC |
| Cores | 4 vCPU | 4 vCPU |
| Memory | 16 GB | 16 GB |
5th Gen AMD EPYC processors deliver \~30% better single-threaded performance over 4th Gen, directly improving build and test execution times.
** Not publicly disclosed by GitHub in [their docs](https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners) but inferred based on publicly available information.*
### 2. Unlimited Concurrency
No concurrency limits means every job gets a runner quicker, even during peak CI loads.
## Run Your Own Benchmark
Compare your actual workflows side by side — run the same steps on a GitHub-hosted runner and a StarSling runner, then compare the timings:
```yaml title=".github/workflows/benchmark.yml"
jobs:
github-hosted:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: time npm ci && time npm run build
starsling:
runs-on: starsling-ubuntu-24.04
steps:
- uses: actions/checkout@v4
- run: time npm ci && time npm run build
```
# Optimizations (/ai-agents/optimizations)
StarSling's AI agents continuously perform deep scans of your workflows and open PRs to optimize your CI speed. See [real examples of AI-authored PRs](https://starsling.dev/#ai-prs) or browse the [customer case studies](https://starsling.dev/customers).
This page covers StarSling's paid, hosted, continuous optimization service. To run a free, local audit on demand with your own coding agent, use [ci-speedup](https://docs.starsling.dev/skills/ci-speedup), or grade your workflow configuration against CI best practices with [ci-score](https://docs.starsling.dev/skills/ci-score).
For new accounts, AI-powered optimization PRs are only available to customers on paid plans and are not enabled by default.
## How It Works
StarSling monitors your CI workflows and identifies opportunities to improve performance, reliability, and efficiency. When an optimization is found, StarSling opens a pull request with the suggested changes and an explanation.
## What Gets Optimized
### Caching
Detect missing or misconfigured caches and add them automatically.
* [Better Auth](https://starsling.dev/customers/better-auth): fixed a Turborepo cache that wasn't hitting between runs and cached Playwright browser installs.
* [Partcl](https://starsling.dev/customers/partcl): cached build artifacts across shards.
### Dependency installation
Suggest faster install strategies (e.g., frozen lockfiles, parallel installs).
### Build steps
Identify redundant or slow steps that can be parallelized or removed.
* [Mastra](https://starsling.dev/customers/mastra): sharded the E2E kitchen-sink suite across 3 parallel jobs.
* [Partcl](https://starsling.dev/customers/partcl): parallelized a test suite the team had never had time to split.
### Test reliability
Replace fixed `sleep` calls with real readiness checks and add service healthchecks.
* [Mastra](https://starsling.dev/customers/mastra): swapped blind sleeps for polling across MongoDB and Chroma and added Docker healthchecks.
* [Better Auth](https://starsling.dev/customers/better-auth): added Docker Compose healthchecks.
### Workflow structure
Recommend job splitting, matrix strategies, and dependency ordering.
### Runner configuration
Flag suboptimal runner labels or resource usage.
* [Partcl](https://starsling.dev/customers/partcl): right-sized the heaviest jobs from 64-core down to 8-core machines.
## How Optimizations Are Delivered
1. StarSling analyzes your workflow runs and configuration
2. When an optimization is identified, a PR is opened with the changes
3. Each PR includes an explanation of what changed and why
4. You review and merge at your own pace
> "Within a day of migrating to StarSling Runners, their agents opened up a PR that literally made our Rust CI tests 2x faster!"
>
> — Vamshi Balanaga, Co-founder & CTO, [Partcl](https://starsling.dev/customers/partcl)
## See It In Action
Real CI optimizations StarSling agents have shipped, with measured before/after numbers:
See all [StarSling case studies →](https://starsling.dev/customers).
# ci-speedup (/skills/ci-speedup)
[ci-speedup](https://github.com/starslingdev/skills) is a free, open-source, MIT-licensed agent skill that runs on your machine, analyzes GitHub Actions, and gives your coding agent evidence to draft a fix. It works independently of StarSling Runners and does not require the StarSling GitHub App.
[See the ci-speedup overview](https://starsling.dev/ci-speedup) for a quick introduction before following the operational guide below.
ci-speedup is a local, on-demand audit that you invoke with your own coding agent. To grade your workflow configuration against CI best practices instead, use [ci-score](https://docs.starsling.dev/skills/ci-score); to scan it for attack vectors, use [ci-secure](https://docs.starsling.dev/skills/ci-secure). [StarSling optimization PRs](https://docs.starsling.dev/ai-agents/optimizations) are a separate paid, hosted service that continuously analyzes your CI and opens reviewable PRs once enabled.
## Before you start
You need:
* The [GitHub CLI](https://cli.github.com/) authenticated to GitHub. Run `gh auth status` to check.
* Read access to the target repository's GitHub Actions runs, jobs, and logs.
* Python 3.9 or newer and [PyYAML](https://pypi.org/project/PyYAML/).
* Node.js and npm to install or invoke the skill with `npx`.
## Install and run
Install the skill with Vercel's Skills CLI:
```bash
npx skills add starslingdev/skills
```
The CLI lets you select the coding agent and whether to install the skill for the current project or globally. It [supports a long list of coding agents](https://github.com/vercel-labs/skills#supported-agents), including Claude Code, Codex, Cursor, and OpenCode.
In a repository, invoke the installed skill from your coding agent:
```text
/ci-speedup
```
The skill confirms the repository before it starts reading run data. To use it without installing, ask your coding agent to run this command and follow the generated instructions:
```bash
npx skills use "https://github.com/starslingdev/skills" --skill "ci-speedup"
```
## On demand versus continuous
| | ci-speedup | StarSling optimization PRs |
| ------------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------- |
| **Delivery** | Agent skill you invoke | Hosted optimization service |
| **Execution environment** | Your machine, coding agent, and authenticated `gh` CLI | StarSling's hosted agents and GitHub App |
| **Cost and availability** | Free, open source, and independent of StarSling Runners | Available on paid plans once enabled; not enabled by default for new accounts |
| **Trigger** | On demand, when you run it | Continuous analysis as workflows change |
| **Output** | Diagnosis and evidence-backed prompt for your coding agent | Reviewable optimization PRs opened by StarSling |
## Resources
# ci-score (/skills/ci-score)
[ci-score](https://github.com/starslingdev/skills) is a free, open-source, MIT-licensed agent skill that runs on your machine, grades a repository's GitHub Actions configuration against CI best practices, and hands your coding agent a concrete fix for every gap. It works independently of StarSling Runners and does not require the StarSling GitHub App.
[See the ci-score overview](https://starsling.dev/ci-score) for a quick introduction before following the operational guide below.
ci-score grades configuration adherence, not speed — a fast repo can hold a low score. For a measured audit of what actually makes your CI slow, use [ci-speedup](https://docs.starsling.dev/skills/ci-speedup); for the ten critical attack vectors, use [ci-secure](https://docs.starsling.dev/skills/ci-secure). All three are local, on-demand skills; [StarSling optimization PRs](https://docs.starsling.dev/ai-agents/optimizations) are a separate paid, hosted service.
## Before you start
You need:
* A full local checkout of the repository you want to score. The score reads workflow YAML, local composite actions, and repo-root build-tool configs, so a partial view understates your setup.
* Python 3.9 or newer and [PyYAML](https://pypi.org/project/PyYAML/).
* Node.js and npm to install or invoke the skill with `npx`.
No network access is used during scoring — everything reads the local tree, and nothing is sent to StarSling.
## Install and run
Install the skill with Vercel's Skills CLI:
```bash
npx skills add starslingdev/skills
```
The CLI lets you select the coding agent and whether to install the skill for the current project or globally. It [supports a long list of coding agents](https://github.com/vercel-labs/skills#supported-agents), including Claude Code, Codex, Cursor, and OpenCode.
In a repository, invoke the installed skill from your coding agent:
```text
/ci-score
```
The skill confirms the repository before it starts scoring. To use it without installing, ask your coding agent to run this command and follow the generated instructions:
```bash
npx skills use "https://github.com/starslingdev/skills" --skill "ci-score"
```
## What the score measures
The CI Score is a pass/fail rubric of eleven configuration facts, each verifiable in your own workflow files in under a minute:
* **Dependency caching** — a cache action or a `setup-*` cache input is configured
* **Build caching** — a build-tool cache (Turbo, Nx, Gradle, sccache, Bazel) is configured
* **Shallow checkout** — no PR-gating checkout pulls full history with `fetch-depth: 0`
* **Test sharding / matrix** — a test job runs a matrix or a shard-like axis
* **Change-scoped builds** — CI scopes work to what changed
* **Concurrency groups** — a PR-triggered workflow declares a concurrency group
* **Superseded runs cancelled** — that concurrency group sets `cancel-in-progress`
* **Path filters** — a PR-triggered workflow scopes itself with `paths` / `paths-ignore`
* **Job timeouts** — jobs set `timeout-minutes` instead of GitHub's 360-minute default
* **Scoped OIDC id-token** — `id-token: write` is granted per job, never workflow-wide
* **Pinned action SHAs** — at least 95% of remote action references are pinned to a commit SHA
A check whose subject doesn't exist in your repo is not applicable and leaves the denominator. Your score is checks passed over checks applicable. The report ranks one fix per failed check by impact × risk, each with a fix recipe and a paste-able agent prompt.
Two caveats the report repeats beside the score: it measures **adherence, not speed**, and it is **not a security audit** — exactly two of the eleven checks (action pinning and OIDC token scoping) happen to be security-related.
## ci-score versus ci-speedup
| | ci-score | ci-speedup |
| ------------ | ----------------------------------------------- | ---------------------------------------------------------- |
| **Question** | Does my CI config follow best practices? | What actually makes my CI slow? |
| **Input** | Your local checkout's workflow YAML and configs | Real run history sampled over the `gh` API |
| **Output** | A score plus one ranked fix per failed check | A measured root-cause report of the merge-gating long pole |
| **Requires** | A full checkout, Python, and PyYAML | The above plus an authenticated `gh` CLI |
| **Runtime** | Seconds, fully offline | Minutes, scaling with repo size |
Both are free, open source, run on your machine, and send nothing to StarSling.
## Resources
# ci-secure (/skills/ci-secure)
[ci-secure](https://github.com/starslingdev/skills) is a free, open-source, MIT-licensed agent skill that runs on your machine, scans a repository's GitHub Actions workflows for the ten critical CI/CD attack vectors, and offers your coding agent a fix for every finding. It works independently of StarSling Runners and does not require the StarSling GitHub App.
[See the ci-secure overview](https://starsling.dev/ci-secure) for a quick introduction before following the operational guide below.
ci-secure is deliberately **not** a comprehensive security audit. It checks critical exploit-chain vectors only — complete outsider-to-compromise paths with real incidents behind them. A clean ci-secure report means those ten paths are closed, not that your CI is secure. The [selection criteria and the rejection record](https://github.com/starslingdev/skills/blob/main/skills/ci-secure/references/why-these-ten.md) document what was left out and why.
ci-secure scans for attack vectors. To grade your workflow configuration against CI best practices, use [ci-score](https://docs.starsling.dev/skills/ci-score); to find what actually makes your CI slow, use [ci-speedup](https://docs.starsling.dev/skills/ci-speedup). All three are local, on-demand skills; [StarSling optimization PRs](https://docs.starsling.dev/ai-agents/optimizations) are a separate paid, hosted service.
## Before you start
You need:
* A full local checkout of the repository you want to scan, with a `.github/workflows/` directory. The skill stops early if the repo has no GitHub Actions workflows.
* Python 3.9 or newer and [PyYAML](https://pypi.org/project/PyYAML/) — the scanner's only third-party dependency.
* Node.js and npm to install or invoke the skill with `npx`.
The [GitHub CLI](https://cli.github.com/) is **optional but recommended**. Four checks need it: the impostor-SHA vector, which cannot be answered from YAML alone, the dormancy note on findings, and two repository settings read over the API. Without `gh`, the scan still runs — the impostor-SHA check reports as skipped, and the two API-gated settings report as unmeasured coverage gaps rather than as passes.
## Install and run
Install the skill with Vercel's Skills CLI:
```bash
npx skills add starslingdev/skills
```
The CLI lets you select the coding agent and whether to install the skill for the current project or globally. It [supports a long list of coding agents](https://github.com/vercel-labs/skills#supported-agents), including Claude Code, Codex, Cursor, and OpenCode.
In a repository, invoke the installed skill from your coding agent:
```text
/ci-secure
```
To use it without installing, ask your coding agent to run this command and follow the generated instructions:
```bash
npx skills use "https://github.com/starslingdev/skills" --skill "ci-secure"
```
## What it scans for
Each of the ten vectors is a complete path from outside your organization to compromise, and each has a documented real-world incident behind it. Every finding renders with a "what an attacker could do" scenario rather than a rule name, so you can judge the risk without reading the detector.
* **Template injection in `run:` blocks** — attacker-controlled text is interpolated into a shell script before the shell ever sees it, so a crafted branch name or issue title executes as code on the runner.
* **Fork code executed with privileges** — a workflow on an untrusted trigger checks out and runs the attacker's head ref while holding secrets and a write token, the classic "pwn request".
* **Cache poisoning from `pull_request_target`** — a job with fork code writes the shared cache, and the poisoned entry is then restored by trusted builds on your default branch.
* **Impostor or unreachable action SHAs** — an action is pinned to a commit that is not reachable from the action's own repository, which looks like the safest possible pin while resolving to code its maintainers never published.
* **Whole-context secret dumps** — `toJSON(secrets)`, `toJSON(github)`, or `toJSON(env)` written into logs or an artifact, exposing every secret at once.
* **`$GITHUB_ENV` and `$GITHUB_PATH` hijack** — attacker-influenced text is appended to those files, letting an attacker set environment variables or prepend a directory to `PATH` for every later step.
* **Write tokens on untrusted triggers** — `pull-requests: write` and similar grants handed to a workflow anyone can trigger from a fork.
* **Credentials in caches and artifacts** — a cache or artifact `path:` that sweeps in known credential files such as `.npmrc`, `.git/config`, or a cloud credentials directory.
* **Unverified remote code execution** — `curl | bash`, and fetch-and-run against a mutable ref, where whoever controls the remote host or branch controls what runs in your job.
* **Dependency install scripts in privileged jobs** — lifecycle scripts from the dependency tree executing in a job that holds secrets, so one compromised transitive package reaches them.
## Configuration hygiene checks
Alongside the vector scan, ci-secure reports eight pass/fail configuration facts. These are hygiene, not exploit chains — a failure is a weakened defense rather than a demonstrated path in:
* **`permissions:` declared** — every workflow declares `permissions`, instead of inheriting a repository default that may be read-write everything.
* **Write grants scoped to jobs** — no workflow-level `write` for any scope except `id-token`, which is [ci-score](https://docs.starsling.dev/skills/ci-score)'s check rather than this one, and no `permissions: write-all`. Write grants belong on the jobs that need them.
* **CODEOWNERS covers workflows** — a CODEOWNERS entry covers `.github/workflows/`, so workflow changes need a specific approval rather than any approval.
* **No fork code checked out on untrusted triggers** — bare untrusted triggers pass; checking out the attacker's ref is what fails.
* **No blanket `secrets: inherit`** — reusable workflows are passed secrets by name, so a called workflow's blast radius is visible in the caller.
* **Checkout credentials not persisted** — untrusted-trigger workflows set `persist-credentials: false`, keeping the token out of `.git/config` where a later step could read it.
* **Required checks cannot be skipped** — every required status check is produced by a job that always runs. GitHub counts a skipped required check as a pass, so a check only a conditional job reports can be satisfied by never running it.
* **Fork-PR approval is effective** — the repository's approval gate covers more than accounts brand new to GitHub, a setting that otherwise gates nobody real.
Two of these read repository settings over the API, so they report as unmeasured rather than as passes when `gh` is unavailable. An unmeasured check is a coverage gap, not a clean bill.
## How the three skills differ
| | ci-secure | ci-score | ci-speedup |
| ----------------------- | ------------------------------------------------------------- | ----------------------------------------------- | ---------------------------------------------------------- |
| **Question** | Can someone outside my org compromise my CI? | Does my CI config follow best practices? | What actually makes my CI slow? |
| **Input** | Workflow YAML, plus four checks over the `gh` API | Your local checkout's workflow YAML and configs | Real run history sampled over the `gh` API |
| **Output** | Findings with attacker scenarios, and a fix offer per finding | A score plus one ranked fix per failed check | A measured root-cause report of the merge-gating long pole |
| **Requires** | A checkout, Python, and PyYAML; `gh` recommended | A full checkout, Python, and PyYAML | The above plus an authenticated `gh` CLI |
| **Zero findings means** | Those ten paths are closed, not that CI is secure | Nothing — the score is the output | No single dominant long pole was found |
All three are free, open source, run on your machine, and send nothing to StarSling.
## What it will and will not do
The skill asks which findings you want fixed and dispatches one subagent per finding group. It **never commits, pushes, or opens a pull request unasked** — by default you review the working-tree diff yourself.
Zero findings is a first-class result, reported as such rather than padded with lower-severity noise.
## Resources
# Overview (/sling-cli)
`sling` is the primary interface to the StarSling platform for agents and humans. The questions you ask most often are each a single command: [why](https://docs.starsling.dev/sling-cli/commands/analyze#sling-why) a job failed, where the [time](https://docs.starsling.dev/sling-cli/commands/analyze#sling-time) went, and what sits at the [top](https://docs.starsling.dev/sling-cli/commands/analyze#sling-top) of your spend.
```console copy="sling time"
$ sling time 93165914090
Job Name: Lint, typecheck, test & spell · Wall-Clock: 58s · Queue-Wait: 2s
PHASES TIME %-SHARE TIMELINE
provision 4.0s 7.14% ██░░░░░░░░░░░░░░░░░░░░░░
checkout+patch 2.0s 3.57% ░░█░░░░░░░░░░░░░░░░░░░░░
steps 46.0s 82.14% ░░░███████████████████░░
Set up Bun 1.0s ░░░█░░░░░░░░░░░░░░░░░░░░
Set up mise tools 2.0s ░░░█░░░░░░░░░░░░░░░░░░░░
Install dependencies 1.0s ░░░░█░░░░░░░░░░░░░░░░░░░
Lint (Biome) 2.0s ░░░░█░░░░░░░░░░░░░░░░░░░
Lint shell (shellcheck) 14.0s ░░░░░██████░░░░░░░░░░░░░
Lint Dockerfiles (hadolint) 0.0s ░░░░░░░░░░░█░░░░░░░░░░░░
▸ Typecheck 18.0s ░░░░░░░░░░░████████░░░░░
Test 7.0s ░░░░░░░░░░░░░░░░░░░███░░
Catalog drift 1.0s ░░░░░░░░░░░░░░░░░░░░░░█░
Spell (typos) 0.0s ░░░░░░░░░░░░░░░░░░░░░░█░
teardown 4.0s 7.14% ░░░░░░░░░░░░░░░░░░░░░░██
```
## Install
On Apple Silicon macOS and x64 glibc Linux:
```bash
curl -fsSL https://runners.starsling.dev/cli/install.sh | sh
```
Then sign in and confirm the install:
```bash
sling login
sling --version
```
`sling login` runs the GitHub device-code flow and saves your session **per user, not per shell** — so an agent running on the same machine inherits it with no further setup.
For `PATH` setup, upgrades, digest verification, and `sling doctor`, see [Installation](https://docs.starsling.dev/sling-cli/installation).
## What you can do with it
* [`sling why`](https://docs.starsling.dev/sling-cli/commands/analyze#sling-why) classifies why a job failed, with evidence and a fix to run.
* [`sling logs`](https://docs.starsling.dev/sling-cli/commands/inspect#sling-logs) filters server-side, returning only the logs that matter.
* [`sling time`](https://docs.starsling.dev/sling-cli/commands/analyze#sling-time) decomposes where the time went, phase by phase.
* [`sling top`](https://docs.starsling.dev/sling-cli/commands/analyze#sling-top) ranks what sits at the top of your spend.
* [`sling usage`](https://docs.starsling.dev/sling-cli/commands/analyze#sling-usage) attributes runner minutes and cost, grouped how you ask.
* [`sling bill`](https://docs.starsling.dev/sling-cli/commands/billing#sling-bill) is a read-only budget check for the open period.
## Why it exists
AI agents working inside CI jobs on StarSling runners, and the humans supervising them, have no first-class way to answer the questions CI work actually consists of: why did this job fail, where does the wall-clock go, what is burning our runner minutes, show me the logs that matter.
Those answers otherwise live behind the GitHub UI (hostile to agents), raw APIs (no diagnosis, no aggregation), and dashboards (exports, not interfaces). Agents end up screen-scraping logs into their context windows, guessing at failure causes, and unable to branch on outcomes without parsing prose. Humans get no leverage from the telemetry StarSling already collects.
## Learn more
# Installation (/sling-cli/installation)
## MacOS and Linux
```bash
curl -fsSL https://runners.starsling.dev/cli/install.sh | sh
```
The installer detects your platform, downloads the matching build, verifies its SHA-256 digest, and activates it. If `~/.local/bin` is not on your `PATH`, the installer prints the `export` line to add.
It supports Apple Silicon macOS and x64 glibc Linux.
Check the install:
```bash
sling --version
```
To upgrade, rerun the same command.
## Get Started
### Authenticate
```bash
sling login
```
This runs the GitHub device-code flow: it opens an approval page, waits for you to confirm the device code, saves the session token to `~/.config/sling/credentials`, and links your default organization.
The credential is stored **per user, not per shell**, with owner-only permissions. So an AI agent running on the same machine as you inherits it: once you have signed in, the agent can run any `sling` command with no further setup. See [Configuration](https://docs.starsling.dev/sling-cli/configuration#how-agents-authenticate) for how that differs in CI.
### Check your identity
```bash
sling whoami
```
Reports your identity, org, plan, token scopes, and token expiry, so you can see exactly what the credential you just saved can reach.
### Verify
```bash
sling doctor
```
`doctor` checks your token, the control plane's reachability, clock skew, git remote, patch tooling, version, org resolution, and whether the [sling agent skill](https://github.com/starslingdev/skills/tree/main/skills/sling) is installed for a coding agent on the machine. Exit `0` means healthy; exit `10` means a real check failed. A row marked `!` is an advisory — a newer CLI, or a coding agent without the skill — and still counts as healthy; `○` marks a check that could not run.
```console
$ sling doctor
✓ token valid session (expires 4 Aug 2026, 17:45)
✓ control_plane reachable (https://runners.starsling.dev)
✓ clock_skew 0s vs server
✓ git_remote origin → https://github.com/acme/api.git
✓ patch_tooling git found (/usr/bin/git)
✓ version up to date (0.1.8)
✓ org starslingdev (paid)
✓ agent_skill sling skill installed for Claude Code
Environment healthy!
```
## Next steps
# Authentication and setup (/sling-cli/commands/auth)
Five commands cover getting `sling` working and confirming it still is.
## `sling login`
```text
sling login [--force] [--clear]
```
Signs in with the **GitHub device-code flow**: requests a code, opens the approval page, waits for you to approve in the browser, saves the credential to `~/.config/sling/credentials`, then makes a best-effort attempt to set your default org. If you are already signed in it short-circuits — verifying the token still works rather than re-running the flow.
The device-code flow is deliberate: it needs no localhost callback, so you can sign in from any terminal, including over SSH.
| Flag | Meaning |
| ----------------------- | --------------------------------------------------------------------- |
| `--force` | Re-authenticate even when a valid session exists. |
| `--clear` | Clear the stored credential; an alias for `logout --yes`. |
| `--agent`, `--no-input` | Refuse: the device-code flow cannot run non-interactively (exit `2`). |
```console
$ sling login
Opened https://runners.starsling.dev/cli-login?user_code=AB58-LRFS in your browser.
Confirm code AB58-LRFS to continue.
Waiting for you to approve…
Sling CLI v0.1.0
✓ Successfully authenticated!
✓ Token saved to ~/.config/sling/credentials
✓ You're now linked to acme organization!
```
An agent on this machine inherits the credential this writes, so signing in once covers both of you. A fresh CI container has no session and no browser: see [how agents authenticate](https://docs.starsling.dev/sling-cli/configuration#how-agents-authenticate).
## `sling logout`
```text
sling logout [--yes]
```
Clears the stored credential **and the persisted default org**, so a stale org cannot leak into the next login under a different account. On a terminal it asks first.
| Flag | Meaning |
| ------------ | ------------------------------------------------------------------------------------ |
| `--yes` | Skip the confirmation — explicit consent to clear. |
| `--agent` | Machine-mode consent; bundles `--yes`. |
| `--no-input` | **Not** consent. Fails with exit `2` rather than prompting when a credential exists. |
```console
$ sling logout
Are you sure you want to sign out? (y/N) y
✓ Successfully signed out!
✓ Token cleared from ~/.config/sling/credentials
```
## `sling whoami`
```text
sling whoami [--json]
```
Your identity, org, plan, token scopes, and token expiry — so you can debug access issues without guessing. Session callers see the default org enriched with its plan; API-key callers also see the key id and its scopes.
| Flag | Meaning |
| ------------------- | ----------------------------------------------------------------------- |
| `--json`, `--agent` | Machine output — the `/api/whoami` payload on stdout, errors on stderr. |
```console
$ sling whoami
Identity
Name Lionel Messi
GitHub leo_messi
Email leomessi@example.com
User ID leo_messi_10
Organization
Default arg
Plan enterprise
Credential
Type session
Expires 1 Aug 2026, 00:00
```
The human table renders on stderr, so stdout stays pipe-clean. Calls [`GET /api/whoami`](https://docs.starsling.dev/api/identity/get-whoami).
## `sling doctor`
```text
sling doctor [--json]
```
Diagnoses a broken setup in one command: **binary version, token validity and scopes, org resolution, git remote detection, control-plane reachability, clock skew, patch-tooling presence**, and whether the [sling agent skill](https://github.com/starslingdev/skills/tree/main/skills/sling) is installed for a coding agent. Exit `0` when healthy, **`10`** when a real check fails — so an agent harness can preflight the environment before starting work.
Each row carries a mark: `✓` passed, `○` **skipped** — the check could not run, like the version comparison when the control plane is unreachable, or `agent_skill` when no coding agent is installed — and `!` **advisory**, a suggestion worth acting on. Only `✗` fails the run.
| Flag | Meaning |
| ------------------- | ---------------------------------------------------------------------------------------------- |
| `--json`, `--agent` | Machine output — `{ checks: [{ key, ok, detail, skipped?, warn?, fix_command? }] }` on stdout. |
```console
$ sling doctor --json
{
"checks": [
{ "key": "token", "ok": true, "detail": "valid session (expires 2026-08-04T17:45:00.000Z)" },
{ "key": "control_plane", "ok": true, "detail": "reachable (https://runners.starsling.dev)" },
{ "key": "clock_skew", "ok": true, "detail": "0s vs server" },
{ "key": "git_remote", "ok": true, "detail": "origin → https://github.com/acme/api.git" },
{ "key": "patch_tooling", "ok": true, "detail": "git found (/usr/bin/git)" },
{ "key": "version", "ok": true, "detail": "up to date (0.1.8)" },
{ "key": "org", "ok": true, "detail": "starslingdev (paid)" },
{ "key": "agent_skill", "ok": true, "detail": "sling skill installed for Claude Code" }
]
}
```
A real failing check carries a `fix_command` and flips the exit code to `10`. A skipped check — `ok: false` with `skipped: true` — is informational and keeps the run healthy.
An **advisory** check is the third state: `ok: true` with `warn: true`, the remedy in `fix_command`, and the exit code still `0`. An outdated binary suggests the installer one-liner; `agent_skill` suggests the skill install when a coding agent is present but the skill is not. With no coding agent on the machine, `agent_skill` is skipped rather than nagging.
```json title="advisory and skipped rows"
{ "key": "version", "ok": true, "warn": true, "detail": "version 0.1.8\n0.1.9 is available, to update:", "fix_command": "curl -fsSL https://runners.starsling.dev/cli/install.sh | sh" }
{ "key": "agent_skill", "ok": true, "warn": true, "detail": "not installed — the sling skill lets your coding agent run sling for you, to install:", "fix_command": "npx skills add starslingdev/skills --skill sling" }
{ "key": "agent_skill", "ok": false, "skipped": true, "detail": "no supported coding agent detected — sling is usable directly" }
```
## `sling org switch`
```text
sling org switch [slug]
```
Sets your **default org** so later commands resolve it without a flag. Pass a slug to persist it directly, or omit it on a terminal for an arrow-key picker. An unknown slug exits `2` and lists your valid orgs.
| Flag | Meaning |
| ------------------- | ------------------------------------------------------------------------------- |
| `slug` | The org to make default. Omit for the interactive picker. |
| `--json`, `--agent` | Machine JSON — `{"defaultOrg":"…"}` on stdout. A missing slug is a usage error. |
| `--no-input` | Refuse the picker when no slug is given (exit `2`). |
```console
$ sling org switch beta
You're now linked to beta organization!
# no slug on a TTY → picker
$ sling org switch
Select your default org (↑/↓, Enter; Esc to cancel):
❯ acme
beta
```
Your org is auto-resolved when it is unambiguous, so this is only needed on multi-org accounts — the point is that they do not pay a flag tax on every call. Either way it is a CLI convenience: the HTTP API never infers an org, and [`org` is a required parameter](https://docs.starsling.dev/api/credentials#org-scoping) on every org-scoped endpoint. Use [`GET /api/orgs`](https://docs.starsling.dev/api/identity/list-orgs) to list the slugs you can use.
# Inspecting CI (/sling-cli/commands/inspect)
Four commands for navigating what actually ran. They share one primitive — polymorphic id resolution — so all of them accept a prefixed id, a bare number, or a pasted GitHub Actions URL.
## Identifiers
| Form | Meaning |
| ------------------ | -------------------------------------------------------------------------------- |
| `run_` | One workflow run. |
| `job_` | One job within a run. |
| `att_.` | One execution of a job. Retries create new attempts; earlier ones are preserved. |
| `runner_` | The GitHub Actions runner a job ran on. |
Bare GitHub ids and **GitHub Actions UI URLs are accepted anywhere** a run, job, or attempt id is — so you can paste a URL straight from the browser without translating identifiers.
Resolution is polymorphic: an id resolves downward to the most specific sensible target. Where several candidates match, the command lists them and exits `2` — it never picks silently, so you are never handed the wrong target.
## `sling resolve`
```text
sling resolve [--target run|job|attempt]
```
Resolves a polymorphic id to its concrete target. This is the primitive the id-taking commands share, exposed on its own so you can see what an id maps to.
| Flag | Meaning |
| ---------------------------- | ----------------------------------------------------------------------- |
| `--target run\|job\|attempt` | Bias resolution to one level when the id is ambiguous. |
| `--json`, `--agent` | Machine output — the resolved target, or `{ candidates }` on ambiguity. |
```console
$ sling resolve run_2990884649
run_2990884649 (run)
acme/api
# ambiguous → candidates (exit 2)
$ sling resolve 88886665361
Ambiguous — 2 candidates; pass one:
run_88886665361 (acme/api)
job_88886665361 typecheck (acme/api)
```
**Exit codes:** `0` resolved · `2` usage or ambiguous · `3` not found · `4` not signed in · `5` control-plane failure. Calls [`POST /api/resolve`](https://docs.starsling.dev/api/resolve/resolve-target).
## `sling runs`
Inspect workflow runs. `list` (alias `ls`) pages newest-first with server-side filters; `show` (alias `get`) renders one run and can block until it concludes.
### `sling runs list`
```text
sling runs list [filters] [--limit n] [--cursor c]
```
| Flag | Meaning |
| --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `--branch`, `--status`, `--conclusion`, `--trigger`, `--workflow-path`, `--label` | Server-side filters; combine freely. |
| `--window`, `--month`, `--from` / `--to` | Time window. Mutually exclusive; passing two is a usage error. |
| `--limit`, `--cursor` | Page size, and the cursor to page with. `has_more` prints the next `--cursor`. |
| `--json`, `--agent` | Machine mode — `{ runs, has_more, next_cursor }` on stdout. |
```console
$ sling runs list --status in_progress --limit 3
RUN WORKFLOW BRANCH EVENT STATUS DUR JOBS CREATED
2990884649 Test main push in_progress — 4 2026-07-23 14:02
2990884101 Prebuild main push queued — 1 2026-07-23 13:58
```
Calls [`GET /api/runs`](https://docs.starsling.dev/api/runs/list-runs).
### `sling runs show`
```text
sling runs show [--wait [--fail-fast] [--wait-timeout 30m]]
```
| Flag | Meaning |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `--wait` | Poll until the run reaches a terminal state, then exit `0` on success or **`10`** on any non-success conclusion. |
| `--fail-fast` | With `--wait`, return as soon as a job fails instead of waiting for the rest. |
| `--wait-timeout ` | Cap the wait, e.g. `30m`. |
| `--json`, `--agent` | Machine mode — the run detail on stdout. |
```console
$ sling runs show run_2990884649 --wait; echo $?
…run concludes…
10 # a non-success conclusion under --wait
```
`--wait` is what lets a script or agent gate on a run. Exit `10` is not an error — the command worked and the *run* failed. See [exit codes](https://docs.starsling.dev/sling-cli/configuration#exit-codes).
**Exit codes:** `0` ok · `2` usage · `3` not found · `4` not signed in · `5` control-plane failure · `10` run concluded non-success under `--wait`. Calls [`GET /api/runs/{id}`](https://docs.starsling.dev/api/runs/get-run).
## `sling jobs`
Inspect workflow jobs. `list` is scoped to **one run** (`--run`) or **one repo** (`--repo` plus a window) — exactly one is required. `show` renders one job; passing a *run* id is a usage error that redirects you to `runs show`.
### `sling jobs list`
```text
sling jobs list --run | --repo [--conclusion c] [window]
```
| Flag | Meaning |
| ---------------------------------------- | ----------------------------------------------------------- |
| `--run ` \| `--repo ` | The scope. Mutually exclusive; one is required. |
| `--conclusion` | Filter by conclusion — `failure`, `success`, and so on. |
| `--window`, `--month`, `--from` / `--to` | Time window, with `--repo`. |
| `--limit`, `--cursor` | Pagination. |
| `--json`, `--agent` | Machine mode — `{ jobs, has_more, next_cursor }` on stdout. |
```console
$ sling jobs list --run 2990884649 --conclusion failure
JOB NAME STATUS LABEL ATTEMPTS DURATION RUN CREATED
88886665361 typecheck failure starsling-ubuntu-24.04 1 2.4m 2990884649 2026-07-23 14:02
$ sling jobs show run_2990884649
That id is a run, not a job — try `sling runs show`. # exit 2
```
A terminal job shows its **conclusion**; an in-flight one shows its **status**. Calls [`GET /api/jobs`](https://docs.starsling.dev/api/jobs/list-jobs) and [`GET /api/jobs/{id}`](https://docs.starsling.dev/api/jobs/get-job).
## `sling logs`
```text
sling logs [--job ] [--grep ] [--since ]
[--timestamps] [--limit ] [--cursor ] [--output-file ]
```
Reads a run, job, or attempt's logs **filtered server-side** — so you fetch the failing lines, not the whole transcript.
| Flag | Meaning |
| ---------------------- | ----------------------------------------------------------------------------- |
| `--job ` | For a run target, restrict to one job leg. |
| `--grep ` | Server-side regex filter, e.g. `--grep '##\[error\]'`. |
| `--since ` | Only lines newer than a trailing duration. |
| `--timestamps` | Prefix each line with its timestamp. |
| `--limit ` | Maximum lines per page. |
| `--cursor ` | Resume from a `next_cursor`. |
| `--output-file ` | Write to a file instead of stdout. |
| `--json`, `--agent` | Emit a JSON page — `{ lines, has_more, next_cursor }` — instead of raw lines. |
```console
$ sling logs job_88886665361 --grep '##\[error\]'
##[error]Process completed with exit code 101.
```
Unusually for `sling`, this command writes to **stdout in both modes**: the human path streams raw lines and auto-follows pages to the end. That makes `| head` and `| less` work naturally — closing the pipe early exits cleanly.
**Exit codes:** `0` ok · `2` usage · `3` not found · `4` not signed in · `5` control-plane failure. Calls [`GET /api/logs/{id}`](https://docs.starsling.dev/api/logs/get-logs).
# Analyzing CI (/sling-cli/commands/analyze)
Five commands that answer analytical questions rather than listing records. Each is a first-class product surface, not a dashboard export.
## `sling usage`
```text
sling usage [--org | --repo ] [--group-by ] [--order-by
]
[--asc | --desc] [window] [--json | --agent]
```
Attributes StarSling runner minutes and dollar cost over a time window.
| Flag | Meaning |
| ------------------- | ------------------------------------------------------------------------------------------------- |
| `--org`, `--repo` | Scope. `--repo owner/name` narrows to one repo; mutually exclusive. Defaults to your default org. |
| `--group-by` | `label` (default) · `workflow` · `job` · `repo` · `day` |
| `--order-by` | `cost` (default) · `minutes` · `jobs` · `key` |
| `--asc` / `--desc` | Sort direction, default `--desc`. Naming both is a usage error. Not valid with `--group-by day`. |
| `--window d` | Trailing window, e.g. `30d`. Defaults to 30 days. |
| `--month ` | A calendar month. |
| `--from` / `--to` | An explicit half-open range `[from, to)`. Both required together. |
| `--json`, `--agent` | Machine mode. |
```console
$ sling usage --org mastra-ai --window 7d --group-by repo
StarSling usage — by repo
2026-07-07 → 2026-07-14
KEY RUNNER MIN JOBS COST $ % TOTAL
mastra-ai/mastra 25459.0 10784 $202.85 89.3%
mastra-ai/docs 3040.2 1120 $24.22 10.7%
```
The window flags are **mutually exclusive** — passing two is a usage error, not a silent precedence rule. A single-row result omits `% TOTAL`, since a lone row's share is always 100%. `--group-by day` renders a burn-down chart instead of a table.
Cost is `minutes × vCPU × $0.002`, where minutes are **billed** minutes — `max(1, ceil(duration / 60s))` per job. A five-second job still costs a minute.
### The `plan` block
Every response carries a top-level `plan` describing the caller's billing state, which shapes both the window and the human chrome on any `--group-by` axis:
| Field | Meaning |
| ------------------------------------------ | ------------------------------------------------------------------------------- |
| `status` | `paid` · `free` · `blocked`. Paid orgs get no bar or banner. |
| `period_source` | `stripe` · `install_cycle` · `calendar_month` · `explicit` — labels the window. |
| `free_minutes_limit` / `free_minutes_used` | Free orgs only — the remaining lifetime grant, drawn as a bar. |
| `blocked_reason` | Blocked orgs only — why jobs will not dispatch, shown as a red banner. |
Fields that do not apply are **omitted, not `null`**. Machine mode passes the whole block through verbatim.
**Exit codes:** `0` ok · `2` usage error · `4` not signed in · `5` control-plane failure. Calls [`GET /api/usage`](https://docs.starsling.dev/api/usage/get-usage).
## `sling top`
```text
sling top [--by ] [--metric
] [--asc | --desc] [--org | --repo]
[-n ] [window] [--json | --agent]
```
Ranks **what burns the most runner time and money** — the hotspots worth optimizing. Groups the same facts as `usage`, but sorts by a chosen metric and adds a period-over-period trend so a regression stands out.
| Flag | Meaning |
| ------------------- | ----------------------------------------------------------------------------------- |
| `--by` | `workflow` (default) · `job` · `label` · `repo` · `branch` |
| `--metric` | `runner-minutes` · `cost` · `jobs` · `p95-duration` · `p99-duration` · `queue-wait` |
| `--asc` / `--desc` | Sort direction, default `--desc`. |
| `--org`, `--repo` | Scope; mutually exclusive. |
| `-n`, `--limit` | Row count. |
| `--json`, `--agent` | Machine mode — ranked rows plus a `local` block. |
```console
$ sling top --by workflow --metric cost --window 30d
sling top — by workflow · cost · last 30d
KEY · WORKFLOW REPO COST TREND
Prebuild acme/api $528.35 ↑55.2%
Test (multiple) $203.11 ↓12.3%
Lint acme/web $44.90 —
```
`(multiple)` marks a key spanning several repos. **TREND** compares against the prior equal-length window, or shows `—` when there is no prior data. Calls [`GET /api/top`](https://docs.starsling.dev/api/top/get-top).
## `sling time`
```text
sling time
sling time --repo [window]
```
Decomposes CI **wall-clock into phases** — so you can see whether time goes to infrastructure or to your workload. Polymorphic, with a different analysis at each level:
* A **job or attempt** splits wall-clock into the phase enum, with a per-step breakdown inside `steps`.
* A **run** adds DAG analysis: the **critical path**, **parallelism efficiency** (busy-runner-seconds ÷ wall-clock × width), and the **blocking job** — the single job whose speedup most reduces run wall-clock, so optimization effort lands where it pays.
* **`--repo`** aggregates p50/p95 per phase, per runner label, over a window — answering whether a repo's time goes to queue, provisioning, cache, or actual work.
| Flag | Meaning |
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `` | A run, job, or attempt id, or a pasted GitHub Actions URL. |
| `--repo ` | The repo aggregate instead of a single target. |
| `--window`, `--month`, `--from` / `--to` | **Only with `--repo`.** Passing one alongside an id is a usage error — a single target already names its window. |
| `--json`, `--agent` | Machine mode. |
```console copy="sling time"
$ sling time 93165914090
Job Name: Lint, typecheck, test & spell · Wall-Clock: 58s · Queue-Wait: 2s
PHASES TIME %-SHARE TIMELINE
provision 4.0s 7.14% ██░░░░░░░░░░░░░░░░░░░░░░
checkout+patch 2.0s 3.57% ░░█░░░░░░░░░░░░░░░░░░░░░
steps 46.0s 82.14% ░░░███████████████████░░
Set up Bun 1.0s ░░░█░░░░░░░░░░░░░░░░░░░░
Set up mise tools 2.0s ░░░█░░░░░░░░░░░░░░░░░░░░
Install dependencies 1.0s ░░░░█░░░░░░░░░░░░░░░░░░░
Lint (Biome) 2.0s ░░░░█░░░░░░░░░░░░░░░░░░░
Lint shell (shellcheck) 14.0s ░░░░░██████░░░░░░░░░░░░░
Lint Dockerfiles (hadolint) 0.0s ░░░░░░░░░░░█░░░░░░░░░░░░
▸ Typecheck 18.0s ░░░░░░░░░░░████████░░░░░
Test 7.0s ░░░░░░░░░░░░░░░░░░░███░░
Catalog drift 1.0s ░░░░░░░░░░░░░░░░░░░░░░█░
Spell (typos) 0.0s ░░░░░░░░░░░░░░░░░░░░░░█░
teardown 4.0s 7.14% ░░░░░░░░░░░░░░░░░░░░░░██
Top Steps
STEP TIME
Typecheck 18.0s
Lint shell (shellcheck) 14.0s
Test 7.0s
Set up mise tools 2.0s
Lint (Biome) 2.0s
```
The **TIMELINE** column places each phase and step on the job's wall-clock, so you can see what ran when and what overlapped — not just what was slow. `▸` marks the single largest contributor. **Top Steps** then ranks the steps outright, which is the list to optimize from: here `Typecheck` and `Lint shell` are 32 of the 46 seconds inside `steps`.
Phases come from a frozen vocabulary shared by the runner hooks and the facts store: `queue_wait`, `provision`, `image_pull`, `cache_restore`, `checkout+patch`, `steps`, `cache_save`, `teardown`. Only the phases with time recorded against them are shown. Percentages are share of phase time, so they sum across the phase rows — the nested steps are a breakdown of `steps`, not extra rows in that sum.
**Exit codes:** as elsewhere, plus `6` for partial — telemetry was incomplete, and the body is still emitted. Calls [`GET /api/time`](https://docs.starsling.dev/api/time/get-time).
## `sling why`
```text
sling why [--json | --agent]
```
A **server-authored diagnosis** — why a CI job failed, so an agent can fix the failure instead of re-deriving it from logs. It is read straight from the facts store: the last failing step, a bounded `##[error]` window, and run context. **No LLM sits in the request path**, so the same job always classifies the same way. A run id diagnoses that run's most-diagnosable job.
Every diagnosis carries a classification, evidence references, suggested actions with ready-to-run commands, and an agent-ready remediation `prompt`.
Classifications: `step_failure` · `hang` · `oom` · `timeout` · `cancelled` · `terminated` · `infra` · `network_egress` · `unknown`.
On a **cancelled** target, `why` identifies the canceller — a user, a concurrency group, or a merge-queue flush — so an external cancellation is not misdiagnosed as a code failure.
```console
$ sling why job_88886665361
JobID: 88886665361 · JobName: test (3)
Failure Reason: The step 8 "Test" step failed (exit code 101).
Evidence
Classification: step_failure
Phase: step 8 "Test"
Logs:
##[error]Process completed with exit code 101
Suggestions:
1. Verify the logs
sling logs 88886665361 --grep '##\[error\]'
```
Every diagnosis carries evidence, a suggested next step, and a ready-to-run `prompt` for a downstream agent. Calls [`GET /api/why`](https://docs.starsling.dev/api/why).
## `sling labels list`
```text
sling labels list [--json | --agent]
```
The catalog of available runner labels and their specs, so `runs-on` mapping and cost hints reference concrete options instead of hardcoded names. The catalog is **static and global** — identical for every caller — so the usual `--org`, `--repo`, and window flags are accepted but inert. `list` is the only subcommand.
```console
$ sling labels list
StarSling runner labels
LABEL CPU MEM (GiB) ARCH $/MIN
starsling-ubuntu-24.04 4 16 x64 $0.008
starsling-ubuntu-24.04-2 2 8 x64 $0.004
starsling-ubuntu-24.04-8 8 32 x64 $0.016
starsling-ubuntu-24.04-16 16 64 x64 $0.032
starsling-ubuntu-24.04-32 32 128 x64 $0.064
starsling-ubuntu-24.04-64 64 256 x64 $0.128
```
| Field | Meaning |
| ------------------- | -------------------------------------------------------------------------- |
| `label` | The `runs-on` runner label. |
| `cpu` | vCPU count. |
| `memory_gb` | RAM in GiB — a strict 4 GiB per vCPU. |
| `arch` | CPU architecture; `x64` today. |
| `price_per_min_usd` | Per-minute cost, `cpu × $0.002` — the same rate `usage` and `bill` charge. |
See [Instance types](https://docs.starsling.dev/runners/instance-types) for the same catalog in context. Calls [`GET /api/labels`](https://docs.starsling.dev/api/labels/get-labels).
# Billing (/sling-cli/commands/billing)
Two read-only commands. Plan and payment changes stay in the dashboard; `sling` never mutates billing state.
## `sling bill`
```text
sling bill [--org ] [--month ] [--json | --agent]
```
A budget snapshot for the current open billing period: runner minutes, cost, credits, amount due, per-runner-label line items, and a run-rate month-end projection.
| Flag | Meaning |
| ------------------- | --------------------------------------------------------------------------------------- |
| `--org` | Scope. Defaults to your default org. |
| `--month ` | A **past** month, resolving to its finalized invoice. Omit for the current open period. |
| `--json`, `--agent` | Machine mode — the `/api/bill` envelope on stdout, wrapped with a `local` block. |
```console
$ sling bill --org partcleda
StarSling bill — current period
Billing period · 2026-07-02 → 2026-08-02
Invoice: pending (issued when the period closes)
Runner minutes 45390.0
Amount $396.91
Projected month-end $848.43
RUNNER LABEL TIME COST
starsling-ubuntu-24.04 41166.0 min $329.33
starsling-ubuntu-24.04-8 4224.0 min $67.58
Plan & payment changes live in the dashboard — this command is read-only.
```
Other window flags — `--window`, `--from`, `--to` — are **rejected** here. A bill covers a fixed billing period, so an arbitrary range is meaningless. Use [`sling usage`](https://docs.starsling.dev/sling-cli/commands/analyze#sling-usage) for arbitrary ranges.
### A closed month
Passing `--month` for a closed period resolves to its finalized invoice: a real invoice id, a status, and `Credit` / `Amount due` in place of the run-rate projection.
```console
$ sling bill --month 2026-06 --org partcleda
StarSling bill — invoice
Billing period · 2026-06-02 → 2026-07-02
Invoice: in_1ToapkFSjWlUgKNBIKavWGIJ
Runner minutes 120978.0
Amount $1055.56
Credit −$527.78
Amount due $0.00
Status paid
```
### Response fields
| Field | Meaning |
| ----------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `invoice_id` | Omitted for the open period; a Stripe invoice id for a finalized month. |
| `status` | `open` for the current period; the Stripe status (`paid`, `open`, …) for a finalized month. |
| `amount_usd` | Gross billed — the Stripe subtotal for an invoice, in-house run-rate cost for the open period. |
| `credits_usd` | All reductions off the gross: a partner discount and/or the free grant. |
| `amount_due_usd` | Still outstanding. `$0` once an invoice is paid. |
| `free_credit_total_usd` / `free_credit_remaining_usd` | Free orgs only — the one-time lifetime grant. Omitted otherwise. |
| `line_items` | Per-runner-label minutes and cost. |
| `projected_month_end_usd` | Run-rate projection for the open period; the finalized net billed for an invoice. |
A field the org does not have — a paid org's grant fields — is omitted rather than erroring. Calls [`GET /api/bill`](https://docs.starsling.dev/api/bill/get-bill).
## `sling bill history`
```text
sling bill history [--org ] [-n ] [--after ] [--json | --agent]
```
Prior **finalized** invoices, newest first. Each carries Stripe's authoritative id, status, and amount, joined with per-label usage.
| Flag | Meaning |
| ---------------------- | --------------------------------------------------------------------- |
| `-n`, `--limit` | How many invoices — 1 to 100, default 6. |
| `--after ` | Cursor. Pages to invoices older than the given id; follow `has_more`. |
| `--json`, `--agent` | Machine mode — `{ invoices, has_more }` on stdout. |
```console
$ sling bill history --org partcleda
StarSling bill — prior invoices (newest first)
INVOICE PERIOD STATUS RUNNER MIN AMOUNT CREDIT DUE
in_1ToapkFSjWlUgKNBIKavWGIJ 2026-06-02 → 2026-07-02 paid 120978.0 $1055.56 −$527.78 $0.00
RUNNER LABEL MINUTES COST
starsling-ubuntu-24.04 112049.0 $896.39
starsling-ubuntu-24.04-8 8775.0 $140.40
AMOUNT is Stripe's gross (before −CREDIT); DUE is what's still outstanding ($0 once paid).
```
Note that `--after` takes an **invoice id**, not the opaque cursor the listing endpoints use. Calls [`GET /api/bill/history`](https://docs.starsling.dev/api/bill/get-bill-history).
## Exit codes
Both commands: `0` ok · `2` usage error · `4` not signed in · `5` control-plane failure. The human view is chrome and goes to stderr, so `sling bill --json | jq` is safe to pipe unconditionally. See [Pricing](https://docs.starsling.dev/pricing) for how the rates are set.
# Configuration (/sling-cli/configuration)
## Environment variables
`sling` is configured entirely through the environment — there are no config files beyond the credential it writes at `~/.config/sling/credentials`.
| Variable | Meaning |
| ------------ | ----------------------- |
| `SLING_HOST` | Control-plane base URL. |
### `SLING_HOST`
Env-only by design — there is no `--host` flag. It must be `https://` (or `http://` on a loopback development host) so the token never travels in cleartext.
## How agents authenticate
**On your own machine, an agent needs nothing extra.** [`sling login`](https://docs.starsling.dev/sling-cli/commands/auth#sling-login) writes the session to `~/.config/sling/credentials` for your user, so any agent running as you picks it up. Sign in once and the agent is authenticated.
See [API credentials](https://docs.starsling.dev/api/credentials) for the credentials the control plane accepts and the scopes each endpoint needs.
## Context resolution
`sling` resolves the org and repo it is acting on so that in-repo invocations need zero addressing:
* **Org** is auto-resolved when unambiguous, overridable with `--org `, and persisted as a default by [`sling org switch`](https://docs.starsling.dev/sling-cli/commands/auth#sling-org-switch). Multi-org accounts do not pay a flag tax on every call.
* **Repo** is detected from the git remote of the current directory, overridable with `--repo `.
This is a CLI convenience. The HTTP API never infers either — [`org` is a required parameter](https://docs.starsling.dev/api/credentials#org-scoping) on every org-scoped endpoint.
## Global flags
Available on the root command and every subcommand.
| Flag | Meaning |
| --------------------- | -------------------------------------------------------------------------------------- |
| `--org ` | Org context. Auto-resolved when unambiguous; persisted default via `sling org switch`. |
| `--repo ` | Repo context. Defaults to the git remote detected in the current directory. |
| `--version`, `-v` | Print the version and exit. |
| `--help`, `-h` | Show help. An unknown command still fails, so a typo does not read as help. |
Machine mode is one switch with individually available parts:
| Flag | Meaning |
| --------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `--agent` | Machine mode. Exactly equivalent to `--json --compact --no-input --no-color --yes`. This is the flag machine callers should pass. |
| `--json` | JSON output on stdout. |
| `--yes` | Assume "yes" for confirmations. |
`sling --help` currently lists only `--json`, `--help`, and `--version` in its flags section — `--agent`, `--compact`, `--no-input`, `--no-color`, and `--yes` all work (confirmed by running each against `sling whoami`) but are not shown. This page is the only place `--agent` is documented today; the CLI owner should add these to `sling --help`.
## Output streams
**stdout carries data only.** Human chrome — summary rows, spinners, prompts — goes to **stderr**. This happens unconditionally: the CLI does not check whether stderr is a TTY, so the same spinner and cursor-control sequences land on stderr even when it is redirected to a file or piped into another process. Don't rely on TTY detection to decide whether chrome is present — pipe stderr to `/dev/null` if you need to suppress it silently. That still leaves the more important case: on a prompt-bearing command like [`sling logout`](https://docs.starsling.dev/sling-cli/commands/auth#sling-logout), `--json` by itself only changes what stdout serializes as — it does **not** imply `--yes`/`--no-input`, so a script passing `--json` alone still blocks on the confirmation prompt. `--agent` is the flag that guarantees non-interactive behavior, since it bundles `--yes` and `--no-input`; reach for `--agent`, not bare `--json`, in anything unattended. A failed command usually writes nothing to stdout: a bad id makes `sling runs show --agent` exit `3` with zero bytes on stdout. `sling logs` is the exception — asked for a real job that stores no logs it exits `3` *and* emits its envelope (`{ "lines": [], "has_more": false, "local": { ... } }`). Branch on `$?`, not on whether stdout is empty.
That is what makes `sling usage --json | jq` safe to pipe unconditionally: on failure `jq` receives empty input rather than half a table.
**JSON casing is set per endpoint, not by whether the field crossed the network.** Nearly every control-plane response `sling` passes through — `top`, `labels`, `usage`, `bill`, `runs`, `jobs`, `resolve`, `why`, `time` — is `snake_case` and frozen, matching its API schema. [`GET /api/whoami`](https://docs.starsling.dev/api/identity/get-whoami) is the confirmed exception: it is a real control-plane call like any other (a bad `SLING_HOST` makes `sling whoami --agent` fail with exit `5`, the same as any other command that reaches the API), but its schema is `camelCase` end-to-end (`userId`, `githubLogin`, `expiresAt`). The `local` envelope described below is composed by the CLI rather than passed through, but every key observed inside it so far is a single lowercase word (`org`, `kind`, `slug`, `plan`), so its casing convention for a multi-word key is not yet established by any shipped response. Don't infer a command's casing from "was this local" — check that command's reference page, or default to expecting `snake_case` and treat `whoami` as the documented exception.
The control-plane response, when there is one, is passed through **verbatim at the top level** of the JSON, with a sibling `local` key recording what the CLI resolved (org, repo, etc.) — not nested inside a wrapper. `local` is not on every response: `sling top`, `sling usage`, `sling runs list` and `sling whoami` all carry it, while `sling labels list` returns `{ "labels": [ ... ] }` with no `local` key at all, so treat it as optional when parsing. For example (values redacted):
```json title="sling top --agent"
{
"by": "workflow",
"metric": "runner-minutes",
"window": { "from": "2026-07-25T22:47:14.510Z", "to": "2026-08-24T22:47:14.510Z" },
"rows": [
{ "key": "ci", "repo": "example-repo", "runner_minutes": 69559, "cost_usd": 556.47, "p50_ms": 69000, "p95_ms": 180000, "p99_ms": 358540, "queue_wait_ms": 14000, "trend_pct": 170.7 }
],
"local": { "org": "example-org" }
}
```
```json title="sling whoami --agent"
{
"identity": { "userId": "example-user-id", "name": "Example User", "email": "user@example.com", "githubLogin": "example-login" },
"credential": { "type": "session", "expiresAt": "2026-08-31T20:38:25.617Z" },
"local": { "org": { "kind": "set", "slug": "example-org", "plan": "paid" } }
}
```
Note the casing split across these two commands: `runner_minutes`, `cost_usd`, and `p50_ms` (the `sling top` control-plane fields) are `snake_case`, while `userId`, `githubLogin`, and `expiresAt` (the `sling whoami` control-plane fields) are `camelCase` — `whoami` calls the control plane the same as `top` does, its response schema is just `camelCase`. In both examples, the `local` block is `camelCase`-shaped regardless of the surrounding response's casing.
[`sling logs`](https://docs.starsling.dev/sling-cli/commands/inspect#sling-logs) differs by mode, and the difference runs the opposite way to the rest of this page. In **human** mode it streams raw log lines to stdout, so `| head` and `| less` behave as you would expect, exiting cleanly when the pipe closes early. Under `--agent` or `--json` it returns a structured envelope like every other command — `{ "lines": [ ... ], "has_more": ..., "local": { ... } }`, one object per line — so a machine caller can hand `logs` to the same JSON parser it uses everywhere else rather than special-casing it.
## Exit codes
Every command exits from one table, so a script or agent can branch on `$?` without parsing output.
| Code | Meaning |
| ---- | --------------------------------------------------------------------------------------------------- |
| `0` | Success. |
| `1` | Unexpected CLI or internal error — reserved for a crash, never a mapped API outcome. |
| `2` | Usage — bad flags, a prompt refused under `--no-input` or `--agent`, or org ambiguity. |
| `3` | Not found — a resolved id has no such run, job, or attempt in this org, or it stores no logs. |
| `4` | Auth — missing, expired, or under-scoped credential. The message includes `sling login`. |
| `5` | Control-plane or API error — a `5xx`, or a transport failure. |
| `6` | Partial — telemetry incomplete; the result is still emitted. Used by `sling time` and `sling why`. |
| `7` | Rate limited — the control plane returned HTTP `429`. |
| `10` | Remote outcome failed — `sling doctor` unhealthy, or `sling runs show --wait` on a non-success run. |
Codes `3`, `4`, `5`, and `7` are mapped by a shared error handler, so any command can surface them. See [API errors](https://docs.starsling.dev/api/errors) for the HTTP responses behind each.
The binary's own `sling exit-codes` help text (as of v0.1.2) documents only codes `0`–`5` — it omits `6`, `7`, and `10`, which are the codes this page documents for `sling time`/`sling why` (partial), rate limiting, and `sling doctor`/`sling runs show --wait` (remote outcome failed). This repo's git history for this page has no earlier version to check when `6`, `7`, and `10` were introduced, so it's unclear whether they're newer than the installed binary's help text or the help text is simply stale. This table is kept as-is (all nine codes) rather than trimmed to match the binary — a healthy local environment couldn't be made to exercise `10` to confirm it directly, but the table's own example below documents it as the unhealthy-`sling doctor` code, and neither this page nor `sling exit-codes` should be trusted blind until the CLI owner reconciles the two.
Exit `10` is not an error. It means the command succeeded and the *answer* was a failure — which is exactly what lets `sling runs show --wait` gate a pipeline on a run's conclusion.
Read a command's exit code with `$?` immediately after it, since the next command overwrites it:
```console
$ sling doctor; echo $?
10 # 0 = healthy, 10 = unhealthy
```
# Overview (/api)
The StarSling control plane exposes an HTTP API for everything the platform knows about your CI: which runs and jobs executed, why one failed, where the wall-clock went, and what it all cost.
Prefer a terminal to an HTTP client? The `sling` CLI wraps these same endpoints, adding argument validation, readable tables, and a stable exit-code contract.
## Base URL
All endpoints are served from a single origin:
Every path in this reference is relative to it, and every path is prefixed with `/api`.
## Conventions
Two things hold across every endpoint:
* **Org scoping.** Results are scoped to the orgs you belong to. Org-scoped endpoints take `org` as a **required** parameter (and often an optional `repo`); a resource in an org you cannot see returns `404`, not `403`, so the API never confirms that an id exists elsewhere.
* **Money and time.** Costs are US dollars in `*_usd` fields. Durations are seconds unless the field name says otherwise, and runner minutes are *billed* minutes, `max(1, ceil(duration / 60s))` per job, so a five-second job still costs a minute.
## Endpoint groups
## Before you start
Two pages cover the behaviour shared by every endpoint. Reading them first saves working it out one `400` at a time:
# Credentials (/api/credentials)
Every endpoint except [`GET /api/info`](https://docs.starsling.dev/api/info/get-control-plane-info) requires a credential. The control plane accepts a bearer token, and rejects anything else with [`401 UNAUTHENTICATED`](https://docs.starsling.dev/api/errors).
## Bearer token
What [`sling login`](https://docs.starsling.dev/sling-cli/commands/auth#sling-login) stores at `~/.config/sling/credentials`, sent in the `Authorization` header:
Anyone holding your token can read everything it has access to. Keep it in a secret store or CI secret. Never commit it, and never put it in a URL, where it leaks into logs and browser history.
## Confirming your credential
[`GET /api/whoami`](https://docs.starsling.dev/api/identity/get-whoami) reports the caller's identity, org, plan, scopes, and token expiry. It is the fastest way to tell an expired token from a scope problem:
## Scopes
Three endpoint groups require a named scope. A credential that lacks it is rejected with [`403 INSUFFICIENT_SCOPE`](https://docs.starsling.dev/api/errors), not `401`: the token was valid, it just could not reach that resource.
| Scope | Required by |
| ------------ | -------------------------------------------------------------------------------------------- |
| `orgs:read` | [`GET /api/orgs`](https://docs.starsling.dev/api/identity/list-orgs) |
| `usage:read` | [`GET /api/usage`](https://docs.starsling.dev/api/usage/get-usage), [`GET /api/top`](https://docs.starsling.dev/api/top/get-top) |
| `bill:read` | [`GET /api/bill`](https://docs.starsling.dev/api/bill/get-bill), [`GET /api/bill/history`](https://docs.starsling.dev/api/bill/get-bill-history) |
Endpoints not listed here need a valid credential but no particular scope. Grant the narrowest set that does the job: a credential that only powers a cost dashboard needs `usage:read` and nothing more.
## Org scoping
Authentication establishes *who* you are; org membership establishes *what* you can see. Results are always scoped to the orgs the caller belongs to.
Every org-scoped endpoint takes `org` as a **required** query parameter. The API never infers it, even when you belong to exactly one org. (The CLI is what makes it feel optional: it fills in the default org you set with `sling org switch`.) Call [`GET /api/orgs`](https://docs.starsling.dev/api/identity/list-orgs) to list the slugs you can use.
A run, job, or attempt belonging to an org you cannot reach returns [`404 NOT_FOUND`](https://docs.starsling.dev/api/errors) rather than `403`. This is deliberate: a `403` would confirm the id exists somewhere, which is itself a disclosure. Do not read a `404` as proof that an id is invalid.
# Errors (/api/errors)
Every failure returns a JSON body with the same shape, so a client can branch on one field rather than parsing prose.
## Error body
```json
{
"code": "INSUFFICIENT_SCOPE",
"message": "This credential is missing the bill:read scope."
}
```
| Field | Type | Notes |
| --------- | ------ | --------------------------------------------------------------------------------------------------------------- |
| `code` | string | A stable machine-readable identifier. Branch on this, never on `message`. |
| `message` | string | Human-readable explanation. Wording may change at any time. |
| `details` | array | Present on validation failures only. Each entry is `{ "path": …, "error": … }`, naming the offending parameter. |
`code` is drawn from a closed set:
| `code` | Meaning |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `VALIDATION` | A parameter was present but unacceptable — an unknown enum value, an out-of-range `limit`, two mutually exclusive window parameters. |
| `MALFORMED_REQUEST` | The request could not be parsed — bad JSON in a `POST` body, or a query string the server cannot read. |
| `UNAUTHENTICATED` | No credential, or one that is expired or invalid. |
| `INSUFFICIENT_SCOPE` | The credential is valid but lacks a required [scope](https://docs.starsling.dev/api/credentials#scopes). |
| `NOT_FOUND` | No such resource *within the orgs you can reach*. |
## Status codes
| Status | Typical `code` | What to do |
| ------ | -------------------- | ------------------------------------------------------------------------------------------------- |
| `400` | `MALFORMED_REQUEST` | Fix the request itself. Retrying unchanged will not help. |
| `401` | `UNAUTHENTICATED` | Supply a credential, or refresh an expired session. See [Authentication](https://docs.starsling.dev/api/credentials). |
| `403` | `INSUFFICIENT_SCOPE` | Reissue the credential with the scope the endpoint names. |
| `404` | `NOT_FOUND` | Check the id — but note it may exist in an org you cannot reach. |
| `422` | `VALIDATION` | Read `details` for the offending parameter and correct it. |
| `500` | — | A control-plane fault. Safe to retry with backoff; every endpoint in this reference is read-only. |
A `404` is deliberately indistinguishable from "exists, but not yours". The API will not confirm that an id exists in an org you cannot see, so treat `404` as "not visible to this credential" rather than "does not exist".
## Retrying
Every endpoint documented here is a read, so retries are safe and cannot double-charge or mutate state. Retry `500` and `429` with exponential backoff; do not retry `400`, `401`, `403`, `404`, or `422` without changing the request, since the outcome is deterministic.
If you are rate limited the control plane returns `429`. Back off before retrying.
## Exit codes in the CLI
The `sling` CLI maps these responses onto a fixed exit-code table, so a script or agent can branch on `$?` without parsing output:
| Exit | Meaning | From |
| ---- | ----------------------------------------------------------- | --------------------- |
| `0` | Success | `2xx` |
| `1` | Unexpected CLI crash | never an API outcome |
| `2` | Usage error — bad flags, conflicting parameters | client-side, or `422` |
| `3` | Not found | `404` |
| `4` | Auth — missing, expired, or under-scoped credential | `401`, `403` |
| `5` | Control-plane or transport failure | `5xx` |
| `6` | Partial — telemetry incomplete, result still returned | `200` with gaps |
| `7` | Rate limited | `429` |
| `10` | Remote outcome failed — e.g. a run concluded unsuccessfully | `200` |
Note that `10` is not an error: the request succeeded and the *result* was a failure. It is what lets `sling runs show --wait` gate a pipeline on a run's conclusion.
# List workflow runs (/api/runs/list-runs)
`GET /api/runs`
Filtered workflow-run listing (sling runs list): branch / status / conclusion / trigger / workflow_path / runner-label / time-window filters over the CI facts store, keyset-paginated via an opaque cursor. Authenticated; scoped to the caller's orgs by membership.
Base URL: `https://runners.starsling.dev`
## Authentication
Requires a bearer token (`Authorization: Bearer`).
## Parameters
| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `org` | query | string | yes | GitHub organization login. Required — a run listing is always org-scoped. e.g. `acme` |
| `repo` | query | string | no | Bare repository name, without the org prefix. Narrows to one repo. e.g. `api` |
| `branch` | query | string | no | Only runs on this branch. e.g. `main` |
| `status` | query | array | no | Lifecycle state. Repeatable: ?status=queued&status=completed. |
| `conclusion` | query | array | no | Terminal outcome. Repeatable, and absent until a run completes. |
| `trigger` | query | string | no | The GitHub event that started the run. e.g. `push` |
| `workflow_path` | query | string | no | Workflow file path, as committed in the repository. e.g. `.github/workflows/ci.yml` |
| `label` | query | string | no | Runner label a job in the run requested. e.g. `self-hosted` |
| `window` | query | string | no | Relative lookback, e.g. 30d. Mutually exclusive with from/to and month. e.g. `30d` |
| `from` | query | string | no | Start of the range, inclusive. Required with `to`. e.g. `2026-06-01` |
| `to` | query | string | no | End of the range, exclusive. Required with `from`. e.g. `2026-07-01` |
| `month` | query | string | no | A whole calendar month, YYYY-MM. e.g. `2026-06` |
| `limit` | query | string \| integer | no | Rows per page. Page with `cursor`. 1–100; e.g. `30` |
| `cursor` | query | string | no | Opaque keyset cursor from a prior page's `next_cursor`. |
## Responses
| Status | Description |
| --- | --- |
| `200` | One page of workflow runs, newest first. Page with `next_cursor`. |
| `400` | The window flags conflict or don't parse, or the cursor is stale. |
| `401` | No credential, or a GitHub grant too old to read org membership — re-run `sling login`. |
| `403` | You are not a member of the org named in `?org=`. |
| `422` | A path or query parameter failed schema validation. `details` names each offending field. |
| `500` | The control plane failed, or the CI facts store is not configured for this deployment. |
### `200` body
- `runs` — array