spotlight_hook

The spotlight_hook resource, and the API's hook kind: a team's rule that runs steps when an event happens to one of the team's tasks.

KindEndpointTerraformAddress
hook/api/resources/teams/{teamId}/hooksspotlight_hook<team key>/<key>

See Resource API for the operations, headers and errors every kind shares.

Example

resource "spotlight_hook" "migrations" {
  team_id = spotlight_team.core.id
  key     = "migrations-need-a-dba"
  on      = "pull-request.opening"
  match = {
    paths = ["db/migrations/**"]
  }
  steps = [
    {
      name    = "review"
      request = { url = "https://dba.acme.dev/review" }
    },
  ]
  signing = {
    oidc = { audience = "https://dba.acme.dev" }
  }
  failure         = "block"
  timeout_seconds = 20
}

Fields

FieldTypeDefaultDescription
keystringrequiredLowercase words joined by dashes, at most 64 characters. Fixed: an update that changes it is refused; Terraform replaces the resource.
onstringrequiredThe event the hook fires on. See events.
match.toolsarray of strings[]Tools, as <integration>.<tool>. Only on tool.using and tool.used. Each must be offered by an integration of the team.
match.linesarray of strings[]Line keys.
match.stationsarray of strings[]Station keys.
match.repositoriesarray of strings[]Repositories, as owner/name. On the pull request events, matched against the pull request's repository; on the others, against any of the line's repositories.
match.pathsarray of strings[]Globs of changed paths. Only on pull-request.opening and pull-request.merged.
whenstringnoneAn expression that returns bool, written bare or as ${{ }}. See Hook positions.
varsmap of name to string{}Values expressions read as vars.<name>. Names start with a letter or _, then letters, digits and _.
stepsarray of objectsrequiredAt least one. See steps.
signingobjectnoneHow requests are signed. See signing. Required when any step makes a request.
failurestringcontinuecontinue or block. What a failed step does to the event. block only on a before event.
timeoutSecondsinteger101 to 30. Bounds each request, and a hook on a before event as a whole.

In Terraform, signing.secret is the write-only signing.secret_wo, and signing.secretVersion is signing.secret_wo_version.

A hook fires when every non-empty match list matches and when holds. An empty list matches everything.

Glob syntax in match.paths:

PatternMatches
**any characters, including /
*any characters except /
?one character except /
a trailing /everything under that directory

Events

EventTimingIn a runtools matchpaths matchEvent fields
tool.usingbeforeyesyesat, tool, inputs
tool.usedafteryesyesat, tool, inputs, output, failed
run.startingbeforeyesat
run.endedafteryesat, outcome
handover.givingbeforeyesat, to, note
pull-request.openingbeforeyesyesat, repository, branch, title, changed
pull-request.mergedafternoyesat, url, repository, mergeCommit, changed
task.openedafternoat
task.movedafternoat, from, to

A before event waits while its hooks run, and a hook can stop it. An after event has already happened; its hooks are queued and run afterwards.

changed is a list of paths. Expressions on an event in a run also see run.id. See Hook values for every value an expression sees.

Steps

FieldTypeDefaultDescription
namestringrequiredLowercase words joined by dashes. Unique within the hook. Later steps read this step as step.<name>.
callstringnoneA tool, as <integration>.<tool>, offered by an integration of the team.
withobjectnoneThe tool's inputs. Only with call. Strings may hold expressions.
requestobjectnoneA request to make.

A step names exactly one of call and request. Steps run in order. A step that fails stops the steps after it.

A step may not call a tool with approval, or a tool of an integration whose connection.holder is requester; the step fails with not-allowed. For the second, the reason is names <tool>, which acts as the person who started a task. A hook acts as nobody.

request:

FieldTypeDefaultDescription
urlstringrequiredWhere the request goes. May hold expressions. https unless from is network.
headersmap of name to string{}Extra headers. May hold expressions. May not be authorization, content-type, host, or start with webhook- or spotlight-.
bodystring, number, boolean, array or objectnoneThe JSON body. Strings may hold expressions. Without it, the event, task and run, as below.
fromstringspotlightspotlight or network. spotlight: Spotlight sends it, to a host on the deployment's outbound allow-list, checked when the hook is written. network: one of the workspace's hosts sends it.

Every request is a POST. Without body, the body is:

{ "hook": "<key>", "on": "<event>", "occurrence": "<id>", "event": {}, "task": {}, "run": { "id": "<run id>" } }

run is null on an event outside a run. A step's output is its response body, parsed as JSON when it is JSON, cut at 16 KiB.

Request headers

On a self-hosted Spotlight, use your deployment's address in place of app.usespotlight.ai.

HeaderValue
content-typeapplication/json
user-agentSpotlight-Hooks/1
webhook-idA message id, the same on every retry of one step of one occurrence.
webhook-timestampUnix seconds.
webhook-signatureWith HMAC signing.
authorizationBearer <token>, with OIDC signing.
spotlight-hookThe hook's key.
spotlight-callback-urlhttps://app.usespotlight.ai/api/hook-callbacks
spotlight-callback-tokenA token for the callbacks.
spotlight-causeThe firing that caused this one, when it has a cause.

Signing

FieldTypeDefaultDescription
signing.secretstringnoneWrite-only. whsec_ followed by 24 to 64 base64-encoded bytes. Required on create and whenever secretVersion changes.
signing.secretVersionintegernoneAt least 1. A change, with a new secret, rotates the secret. A read returns only this. Required with secret.
signing.oidc.audiencestringnoneThe token's aud. Required with oidc.

signing is either { secret, secretVersion } or { oidc }.

HMAC

The signature follows Standard Webhooks:

webhook-signature: v1,<base64 of HMAC-SHA256(key, "<webhook-id>.<webhook-timestamp>.<body>")>

The key is the bytes the part after whsec_ decodes to.

OIDC

The request carries Authorization: Bearer <token>, an ES256 JWT:

ClaimValue
isshttps://app.usespotlight.ai/api
audsigning.oidc.audience
subhook:<workspace id>/<team id>/<hook key>
iat, expThe token lives 5 minutes.
jtiA fresh id.
occurrenceThe occurrence of the event.
webhook_idThe webhook-id header.
body_sha256base64url of the SHA-256 of the body.
DiscoveryAddress
OpenID configurationhttps://app.usespotlight.ai/api/.well-known/openid-configuration
Keyshttps://app.usespotlight.ai/api/.well-known/jwks.json

A hook with oidc is refused when the deployment has no key to sign hook tokens with, in HOOK_SIGNING_KEY: needs HOOK_SIGNING_KEY set to sign OIDC tokens.

Refusing an event

A hook on a before event refuses the event when:

  • a step's response is 2xx with a JSON body whose refuse is a non-empty string, whatever failure is; or
  • a step fails and failure is block.
{ "refuse": "Migrations need a DBA's review first." }

The event is refused with Hook <key> refused. Reason: <reason>, or Hook <key> failed at step <step> and blocks on failure. Reason: <reason>. With continue, a failed step lets the event go on.

The hooks on one before event have 60 seconds together.

Callbacks

The token in spotlight-callback-token lives 15 minutes and is sent as Authorization: Bearer <token>.

EndpointBodyReturns
GET /api/hook-callbacks/task200 with the task: id, title, url, line, station, stationKey, marks, repositories, pullRequest, hook.
POST /api/hook-callbacks/notes{ "body": "<text>" }, 1 to 20,000 characters204. The note appears on the task as said by the hook.

POST /api/hook-callbacks/notes takes an optional Idempotency-Key; a retry with the same key is said once. A missing, expired or unknown token is refused with 401. An empty or too long note is refused with 400.

Failures

CodeCauseRetried
unreachableNothing responded, the request got no response within the time left, or no host of the workspace took a network request in time.yes
timeoutThe hook's time ran out before the step began, or a called tool did not finish within it.yes
throttledResponded 408, 425 or 429.yes
server-errorResponded 5xx.yes
tool-failedThe called tool failed.yes
erroredAnything else went wrong while the step ran.yes
rejectedResponded with any other status that is not 2xx.no
not-allowedThe request was not https, went to a host that is not allowed, could not be signed, or the tool is not offered, needs approval, or acts as the person who started a task.no
unwritableAn expression in the step could not be evaluated.no
brokenThe hook no longer reads as it was written.no
loopThe loop guard stopped it.no
supersededThe failure is not reported, because the task has moved on from the station the hook fired at, has finished, or is gone.no

A hook on an after event is retried up to 8 attempts in all, from 2 seconds apart doubling to 1 hour. A hook on a before event is not retried.

Loop guard

Every firing of a hook on an after event carries the chain of firings and events that caused it. A webhook delivery to Spotlight that carries the spotlight-cause header it was sent, or a callback, joins that chain. A firing is stopped with loop when:

StopCondition
repeatedThe hook would fire again for something already in its chain.
too deepThe chain is already 8 causes deep.
recurringThe hook fired 3 times for the same change in the last 10 minutes.

A stopped firing is reported on the task.

On this page