Workload identity: GitHub Actions

⚠️
Preview — this feature is being built; the design below is the spec.

GitHub Actions can give every job a short-lived OIDC token that says which repository, branch and workflow it is running. Trading that token for a Syntic access token means your workflows call the Syntic API without a stored SYNTIC_API_KEY secret. How the exchange works is on Workload identity.

Claims you can match on

GitHub’s tokens carry claims about the run. The useful ones for trust rules:

ClaimExampleUse it to
repositoryacme/billingTrust one repository.
repository_owneracmeCombine with other conditions; never use alone.
refrefs/heads/mainTrust only the main branch, or refs/tags/v* for releases.
environmentproductionTrust jobs that use a protected GitHub environment.
workflow_refacme/billing/.github/workflows/nightly.yml@refs/heads/mainTrust one specific workflow file.
event_namepush, scheduleExclude pull_request runs from forks.
subrepo:acme/billing:ref:refs/heads/mainA compact identifier combining several of the above.

Set it up

Create a service account

In the Console, open the workspace’s Service accounts and create one, for example gha-billing-nightly, with only the abilities it needs (usually inference).

Add a trust rule

{
  "issuer": "https://token.actions.githubusercontent.com",
  "audience": "https://app.syntic.ai",
  "conditions": {
    "repository": "acme/billing",
    "ref": "refs/heads/main",
    "event_name": "schedule"
  },
  "max_token_lifetime_seconds": 900
}

This trusts only scheduled runs on main of one repository. A pull request, a fork or another repository in the same organization gets 400 invalid_grant.

Allow the job to request a token

Give the job the id-token: write permission. Without it, GitHub does not issue OIDC tokens.

Exchange the token in the workflow

name: nightly-summary
on:
  schedule:
    - cron: "0 5 * * *"
 
permissions:
  id-token: write
  contents: read
 
jobs:
  summarise:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Get a Syntic access token
        run: |
          JWT=$(curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
            "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://app.syntic.ai" | jq -r .value)
          TOKEN=$(curl -sS https://app.syntic.ai/v1/oauth/token \
            -H "syntic-beta: token-exchange-2026-11-01" \
            -d grant_type=urn:ietf:params:oauth:grant-type:token-exchange \
            -d subject_token_type=urn:ietf:params:oauth:token-type:jwt \
            -d service_account=svcacct_01JCB3D5F7H9K1M3P5R7 \
            --data-urlencode subject_token="$JWT" | jq -r .access_token)
          echo "::add-mask::$TOKEN"
          echo "SYNTIC_API_KEY=$TOKEN" >> "$GITHUB_ENV"
 
      - name: Summarise yesterday's issues
        run: python scripts/summarise_issues.py   # reads SYNTIC_API_KEY as usual

::add-mask:: keeps the token out of logs. Later steps use it exactly like an API key until it expires (15 minutes here).

Good rules for GitHub

  • Always include repository; repository_owner alone would trust every repository in the organization.
  • Pin ref for anything that touches production, and prefer protected environment names with required reviewers.
  • Do not trust pull_request events for service accounts with write abilities; code in a pull request can be changed by whoever opened it.
  • Use one service account per workflow purpose, so the audit log shows which workflow made each call.

Each exchange is recorded as service_account.token_issued with the token’s sub, so every API call can be traced back to the workflow run that made it.

Troubleshooting

ErrorCause
invalid_grant naming audThe token was requested without &audience=https://app.syntic.ai.
invalid_grant naming a claimThe run does not match the rule, for example a different branch or event.
Empty JWTThe job lacks permissions: id-token: write.
invalid_clientThe service account id is wrong or the account is disabled.
Was this page helpful?