Docs
Search docs

Create a runner enrollment token

POST/api/workspaces/{workspace_id}/runner-enrollments

createRunnerEnrollment

Mint a single-purpose `nse_` token that registers runners from a fixed template: this workspace, the binding, the allowed backends and whether the runners are ephemeral (ADR 0153). The token is returned once. An actor-bound enrollment binds its runners to the caller and needs workspace membership; a workspace-wide one needs workspace admin. The enrollment works only while its creator still holds that permission and, when a token created it, while that token is live. `hosted` is refused (400).

Requires authentication using a bearer token or a session cookie — see tokens and scopes.

curl

curl -X POST 'https://neosource.dev/api/workspaces/WORKSPACE_ID/runner-enrollments' \
  -H 'Authorization: Bearer $NEOSOURCE_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"actor_bound":false,"vm_backends":[]}'

fetch

fetch("https://neosource.dev/api/workspaces/WORKSPACE_ID/runner-enrollments", {
  method: "POST",
  headers: {
    Authorization: "Bearer $NEOSOURCE_TOKEN",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({"actor_bound":false,"vm_backends":[]}),
});

Path parameters

workspace_idrequired

string

Request bodyrequired

application/json

CreateRunnerEnrollmentRequest

object

Create a runner enrollment token (`POST /api/workspaces/{workspace_id}/runner-enrollments`). Every runner registered with the token gets exactly this template; the agent chooses only its labels and one backend from `vm_backends`.

actor_boundrequired

boolean

`true`: every runner is bound to YOU (it claims only runs you triggered) and needs only workspace membership. `false`: the runners claim any job in the workspace, which needs workspace admin.

ephemeral

boolean

Mint session runners (`neo push --runner`) instead of standing ones.

expires_in_secs

integer | nullint64

Seconds until the token stops working, 60–2592000 (30 days); default 3600.

max_uses

integer | nullint32

Registrations the token admits, 1–1000; default 1. Re-registering a machine (same `node_id`) uses one too.

scope
one of
  • RunnerScope

    string

    Whether a runner is hosted by the platform (per [ADR 0014]) or long-poll-attached from outside.

    "hosted""self_hosted""local"

  • null

vm_backendsrequired

array

Backends a registration may choose from; at least one. The retired and reserved backends are refused as at registration.

items

VmBackendKind

string

"docker_shell""kata_clh""kata_fc""lib_krun""k8s_runc""cloud_hypervisor"

Responses

201Enrollment created

application/json

CreateRunnerEnrollmentResponse

object

enrollmentrequired

RunnerEnrollmentResponse

object

One runner enrollment. Never carries the token.

actor_id

string | null

The account actor-bound runners are bound to (always the creator).

bindingrequired
RunnerBindingKind
created_atrequired

integerint64

created_by_accountrequired

string

The account that created it. Registration re-checks that this account still holds the creating permission.

created_by_token_id

string | null

The token that created it, when one did: registration also needs that token to still be live and to still permit the action.

ephemeralrequired

boolean

expires_at

integer | nullint64

Epoch ms; `null` = never.

idrequired

string

max_uses

integer | nullint32

`null` = unbounded.

revoked_at

integer | nullint64

Epoch ms; `null` = live.

scoperequired
RunnerScope
usesrequired

integerint32

vm_backendsrequired

array

workspace_id

string | null

tokenrequired

string

The `nse_` enrollment token. Returned **exactly once**; only its hash is stored. Pass it to `ns-runner register --enroll` or `neo runner register --enroll`.

403Forbidden — one of: forbidden, needs_scope

application/json

one of
  • ErrorForbidden
  • NeedsScopeError

    object

    `403` body returned when a classic OAuth token is asked for something its granted scopes do not cover — an explicit private listing without `repo`, or the one-click GitHub mirror without `admin:public_key`. `scope` names the missing grant so the client can say exactly what the token lacks. There is no elevation route to widen it any more (a re-link goes through the GitHub App, whose tokens carry no scopes), so the client's answer is to say so, never to start a handshake.

    errorrequired

    string

    Always `needs_scope` — this body exists to carry the extra fields that kind needs.

    "needs_scope"

    scoperequired

    string

    The provider scope to request via elevation (e.g. `"repo"`).

Standard errors

Bodies documented once for the whole API — see standard errors.

  • 400Bad Request — one of: invalid_input
  • 401Authentication required
  • 404Not Found — one of: not_found
  • 409Conflict — one of: already_exists, conflict
  • 429Rate limited — retry after the `Retry-After` header
  • 500Internal server error
  • 503Service temporarily unavailable / at capacity — retry after the `Retry-After` header
  • 504Gateway timeout — the request exceeded the server's handling budget

Schemas

Referenced above. Listed here rather than expanded inline, so the same definition is not repeated at every level.

RunnerBindingKind

string

Which trust binding a job executed under (ADR 0056 §1/§7). Serialized on the wire and in `workflow_run_jobs.executed_on` as the `snake_case` strings `hosted` / `workspace` / `actor`.

"hosted""workspace""actor"

RunnerEnrollmentKind

string

How an enrollment is presented (ADR 0153 §1). Only `Token` is creatable today; `Workload` is the projected-ServiceAccount-token kind, whose columns exist but which no route creates or redeems yet.

"token""workload"

RunnerScope

string

Whether a runner is hosted by the platform (per [ADR 0014]) or long-poll-attached from outside.

"hosted""self_hosted""local"

VmBackendKind

string

"docker_shell""kata_clh""kata_fc""lib_krun""k8s_runc""cloud_hypervisor"