spotlight_line

The spotlight_line resource, and the line resource of the API: a team's workflow, made of its stations, the reactions that move tasks between them, and the repositories its runs work in.

KindEndpointTerraformAddress
line/api/resources/teams/{teamId}/linesspotlight_line<team key>/<key>

The request body is the line definition. A read returns teamId, key and definition, the definition with every default filled in, beside the common fields. See Built-in lines for the lines Spotlight opens for the first team of a workspace created in the web app.

In Terraform, each element of repositories, stations and reactions is a repository, station or reaction block, and reviews, replayCheck and source are reviews, replay_check and source blocks. Within a station block, judge is a judge block and each element of alerts is an alert block.

Example

resource "spotlight_line" "core_delivery" {
  team_id = spotlight_team.core.id
  key     = "delivery"
  name    = "Delivery"
  role    = "delivery"
  runners = ["vpc-internal"]

  repository {
    name  = "acme/api"
    image = "ghcr.io/acme/api-runner:main"
    setup = "npm ci"
  }

  station {
    key       = "backlog"
    name      = "Backlog"
    kind      = "backlog"
    passes_to = ["implementation"]
  }

  station {
    key       = "implementation"
    name      = "Implementation"
    harness   = "claude-code"
    profile   = spotlight_harness_profile.core_careful.key
    prompt    = file("${path.module}/prompts/core/delivery/implementation.md")
    check     = "npm test"
    diffs     = true
    decisions = true
    memories  = true
    tools     = ["${spotlight_integration.core_acme.source}.comment", "pull-request"]
    skills    = [spotlight_skill.core_grafana.key]
    runners   = ["gpu"]
    passes_to = ["code-review"]

    judge {
      rubric = file("${path.module}/judges/core/delivery/implementation.md")
      sample = 0.2
      trust  = 0.6
    }

    alert {
      measure  = "sent_back_rate"
      rises_by = 0.2
      over     = 20
    }
  }

  station {
    key       = "code-review"
    name      = "Code review"
    harness   = "claude-code"
    passes_to = ["implementation"]
    ends      = true
  }

  station {
    key  = "completed"
    name = "Completed"
    kind = "completed"
  }

  reaction {
    happening = "github.checks.failed"
    from      = "implementation"
    to        = "implementation"
  }

  replay_check {
    most_regressions = 0
    ceiling          = 10
    currency         = "USD"
    min_gain         = 0.05
  }

  source {
    repository = "acme/platform"
    path       = "teams/core/delivery.tf"
  }
}

The same line as a request body:

{
  "key": "delivery",
  "name": "Delivery",
  "role": "delivery",
  "runners": ["vpc-internal"],
  "repositories": [
    { "name": "acme/api", "image": "ghcr.io/acme/api-runner:main", "setup": "npm ci" }
  ],
  "stations": [
    { "key": "backlog", "name": "Backlog", "kind": "backlog", "passesTo": ["implementation"] },
    {
      "key": "implementation",
      "name": "Implementation",
      "harness": "claude-code",
      "profile": "careful",
      "prompt": "Implement the task.",
      "check": "npm test",
      "diffs": true,
      "decisions": true,
      "memories": true,
      "tools": ["acme.comment", "pull-request"],
      "skills": ["grafana"],
      "runners": ["gpu"],
      "passesTo": ["code-review"],
      "judge": { "rubric": "Did the run meet the task?", "sample": 0.2, "trust": 0.6 },
      "alerts": [{ "measure": "sent_back_rate", "risesBy": 0.2, "over": 20 }]
    },
    {
      "key": "code-review",
      "name": "Code review",
      "harness": "claude-code",
      "passesTo": ["implementation"],
      "ends": true
    },
    { "key": "completed", "name": "Completed", "kind": "completed" }
  ],
  "reactions": [
    { "happening": "github.checks.failed", "from": "implementation", "to": "implementation" }
  ],
  "replayCheck": { "mostRegressions": 0, "ceiling": 10, "currency": "USD" },
  "source": { "repository": "acme/platform", "path": "teams/core/delivery.tf" }
}

A reviews block, on a line whose role is review:

reviews {
  start   = "opened"
  mention = "@spotlight"
  label   = "spotlight"

  skip {
    spotlight = true
    drafts    = true
    authors   = ["dependabot[bot]", "renovate[bot]"]
  }
}

Line fields

FieldTypeDefaultDescription
keystringrequiredNames the line within its team. Lowercase words joined by dashes, at most 64 characters. Fixed: an update that changes it is refused; Terraform replaces the resource.
namestringrequiredWhat the line is called. At most 100 characters.
rolestringnoneThe role the line plays for its team, as in delivery or review. Lowercase words joined by dashes, at most 64 characters. A role names one line per team.
repositoriesarray of objects[]The repositories the line works on. See repositories.
stationsarray of objectsrequiredThe stations, in order. At least one. See stations.
reactionsarray of objects[]Moves a task when something happens. See reactions.
reviewsobjectnoneThe review policy: which pull requests the line reviews. Only on a line whose role is review. See review policy fields.
runnersarray of strings[]Runner labels the line's work is placed on. Empty: any runner of the workspace qualifies. See placement.
replayCheckobjectnoneHow a planned change of a station on this line must do to pass spotlight replay check. Without it, no planned change of the line is checked. See replay check fields.
sourceobjectnoneWhere the line's Terraform lives, which Spotlight opens a pull request against when a person picks a search candidate. See source fields.

Station fields

FieldTypeDefaultDescription
keystringrequiredNames the station within the line, and is what a run hands a task on to. Lowercase words joined by dashes, at most 64 characters. Not review or done.
namestringrequiredWhat the station is called on the board. At most 100 characters.
kindstringworkbacklog, work or completed.
colorstring#a3a8b0A six-digit hex colour, as in #6d78d5. Stored in lower case.
descriptionstringnoneWhat the station is for. At most 500 characters.
harnessstringnoneThe harness that works tasks here: claude-code, cursor or codex. At most 60 characters. A station without one runs nothing, and no run may hand a task on to it.
modelstringnoneThe model the harness uses. At most 60 characters. Without it, the harness uses its own default.
effortstringnonelow, medium, high, xhigh or max. Without it, the harness uses its own default.
profilestringnoneThe key of a team harness profile. A run at a station naming a profile the team does not hold is refused.
promptstringnoneMarkdown handed to the agent that works the station. At most 20,000 characters.
checkstringnoneA command each layer of a change stack drafted at this station is checked with, as in npm test. One line of at most 500 characters. See checks.
runnersarray of stringsnoneRunner labels this station's work is placed on. Without it, the line's runners apply. See placement.
diffsbooleanfalseWhether a change stack is drafted from each run's diff. See Change stacks and decisions.
decisionsbooleanfalseWhether decisions are drafted from each run. See decisions.
memoriesbooleanfalseWhether proposed team memories are drafted from each completed run, for the team to approve in the inbox. They come from what the agent said, thought and planned, never tool output, and from what people said on the task since the last draft. See memory.
toolsarray of strings[]The tools given to the agent. See tool names. Each tool once.
skillsarray of strings[]The skills given to the agent: a team skill key, or a catalog skill as catalog.<name>@<x.y.z>. Each skill once. A key starting spotlight- is refused.
passesToarray of strings[]The keys of the stations a run here may hand a task on to.
endsbooleanfalseWhether a run here may finish the task by handing over with done. From any other station, done goes to a person instead.
judgeobjectnoneScores the station's runs against a rubric, a list of claims, or both. Only on a work station. See judge fields. A read leaves it out when none is set.
alertsarray of objects[]Thresholds on a measure of the station's runs, kept in the order written. Only on a work station. See alert fields.

Spotlight checks catalog tools and skills against what the workspace's plan includes when the line is written. It does not check at write time that a harness exists or that a profile, a team skill or an integration tool is there.

Tool names

FormExampleTool
a built-in namepull-request, review-comment, reviewSpotlight's own.
<integration>.<tool>acme.commentA tool an integration of the team offers.
catalog.<name>@<x.y.z>catalog.web-search@1.1.0A catalog tool at an exact version.

See Built-in tools and skills for the built-in and catalog tools, and Agent commands for how an agent calls a tool.

Judge fields

FieldTypeDefaultDescription
rubricstringnoneWhat a good run looks like, in Markdown. Not empty, at most 20,000 characters. In Terraform, usually read with file(). Required when claims is not set.
claimsarray of stringsnoneCheckable claims the judge answers yes or no about each run, in order: 1 to 20, each not empty and at most 500 characters, with surrounding spaces trimmed. Required when rubric is not set.
passSharenumber1The share of claims a replayed run must meet to pass, from 0 to 1. 1 means every claim. Only with claims. Terraform: pass_share.
samplenumberrequiredThe share of runs judged, from 0 to 1.
trustnumberrequiredThe agreement with people the judge needs before its scores show, from 0 to 1: Cohen's kappa on a yes-or-no rubric and on claims, Spearman's ρ on a 1 to 5 rubric. See Agreement.

A judge has sample, trust, and rubric, claims or both. See Judging for which runs are judged and when scores show.

A judge with claims scores claim by claim: its score is the share of claims met. A rubric beside them is given to the judge with the claims, and needs no scores: line. passShare decides only whether a replayed run passes; see Results. A read returns claims and passShare only when the judge has claims, and rubric only when it has one.

judge {
  claims = [
    "The change adds a test that fails without it.",
    "The change touches only what the task names.",
  ]
  pass_share = 1
  sample     = 0.2
  trust      = 0.6
}

Without claims, the rubric names its scale on its first line that is not blank, or on a line of a front matter block that opens the rubric between two --- lines:

LineScale
scores: yes-noYes or no.
scores: 1-5A whole number from 1 to 5.
noneYes or no.

scores is matched without regard to case, with any spaces around the colon. A scores: line further down is not read.

An export writes each rubric to judges/<team>/<line>/<station>.md and reads it with file(). It writes claims in the block, one claim to a line, with pass_share.

The judge is part of the station's config: a run records one SHA-256 of its rubric, claims and pass share together, its rubric version, with sample and trust. A change to any of them changes the config fingerprint, and a change to the rubric, a claim or the pass share starts a new rubric version.

MessageCause
Judge rubric must not be empty.rubric is empty or only whitespace.
Judge rubric must be at most 20000 characters.rubric is longer.
Judge sample must be a share from 0 to 1.sample is outside 0 to 1.
Judge trust must be a share from 0 to 1.trust is outside 0 to 1.
Judge rubric names its scale as "scores: yes-no" or "scores: 1-5" on its first line.The judge has no claims, and the scores: line names another scale, or the rubric opens a front matter block with --- and never closes it.
Judge rubric or claims must be given.Neither rubric nor claims is set.
Judge claims must list 1 to 20 claims.claims is empty or has more than 20.
Judge claims each must not be empty.A claim is empty or only whitespace.
Judge claims each must be at most 500 characters.A claim is longer.
Judge passShare needs claims to count.passShare is set without claims.
Judge passShare must be a share from 0 to 1.passShare is outside 0 to 1.

Each is returned with 422 at the path stations[<n>].judge.<field>.

Alert fields

FieldTypeDefaultDescription
measurestringrequiredsent_back_rate, accepted_rate, checks_failed_rate, failed_rate, merged_rate or cost_per_run.
risesBynumbernoneThe measure over the last over runs is this much higher than over the over runs before.
fallsBynumbernoneThe measure over the last over runs is this much lower than over the over runs before.
abovenumbernoneThe measure across the last over runs is higher than this.
belownumbernoneThe measure across the last over runs is lower than this.
overnumberrequiredHow many runs are compared, and how many further settled runs an alert that went off must stay within its threshold to recover. A whole number, at least 1.

In Terraform, risesBy and fallsBy are rises_by and falls_by. An alert sets exactly one of risesBy, fallsBy, above and below; a read returns the others as null.

MeasureUnitrisesBy, fallsByabove, below
The five _rate measuresA share from 0 to 1, so 0.2 is 20 percentage points.Above 0, at most 1.0 to 1.
cost_per_runAn amount in the workspace's currency.Above 0.0 or more.

A station may have any number of alerts, but not two on the same measure with the same kind of threshold: of two alerts that both set risesBy on sent_back_rate, the later one is refused. Alerts are not part of the station's config, so changing them does not change the config fingerprint. Alerts are kept with the line, exported, listed on the Station page, and checked as the station's runs end and settle. See Station alerts.

MessageCause
Alert measure must be one of sent_back_rate, accepted_rate, checks_failed_rate, failed_rate, merged_rate, cost_per_run.measure is another value.
Alert must set exactly one of risesBy, above, fallsBy, below.None, or more than one, is set. Returned at the path of the alert.
Alert "rises by" value must be a share above 0 and at most 1.risesBy on a rate is out of bounds.
Alert "falls by" value must be a share above 0 and at most 1.fallsBy on a rate is out of bounds.
Alert "is above" value must be a share from 0 to 1.above on a rate is out of bounds.
Alert "is below" value must be a share from 0 to 1.below on a rate is out of bounds.
Alert "rises by" value must be an amount above 0.risesBy on cost_per_run is 0 or less.
Alert "falls by" value must be an amount above 0.fallsBy on cost_per_run is 0 or less.
Alert "is above" value must be an amount of 0 or more.above on cost_per_run is below 0.
Alert "is below" value must be an amount of 0 or more.below on cost_per_run is below 0.
Alert must compare a whole number of runs, at least 1.over is not a whole number of at least 1.
Alert for "<measure> <rises by, is above, falls by or is below>" already exists on this station.An earlier alert at the station has the same measure and sets the same one of risesBy, above, fallsBy and below. Returned at the path of the later alert.

Each is returned with 422 at the path stations[<n>].alerts[<n>], followed by .<field> where the message names one. Paths and messages use the API's field names, as in risesBy, also for a line written in Terraform.

Repository fields

FieldTypeDefaultDescription
namestringrequiredowner/name. Each repository once per line.
imagestringnoneThe runner image every station of the line runs in, as in ghcr.io/acme/api-runner:main. Without it, the host's default image. Lower case except the tag, at most 255 characters, with an optional tag or @sha256: digest. The image is built FROM ghcr.io/usespotlightai/spotlight-runner.
setupstringnoneA command run in the checkout, as the agent, before each run's agent starts, and before a change stack check wherever a layer changes a lockfile. One line of at most 500 characters.

The repositories of one line name at most one distinct image. A read returns repositories sorted by name.

Reaction fields

FieldTypeDefaultDescription
happeningstringrequiredWhat happens, source first, as in github.checks.failed. Matches ^[a-z0-9]+(\.[a-z0-9][a-z0-9_-]*)+$.
fromstringnoneThe key of the station the task must stand at for the reaction to apply. Without it, the reaction applies at any station.
tostringrequiredThe key of the station the task moves to. While a run holds the task, the move waits until the run ends, and is dropped if the run passes the task on. A move to the completed station instead stops the run and asks a person, or, if the task awaits an answer to the run's question, adds a note to it. See Moves.

See GitHub events for the happenings GitHub raises. An integration raises its own.

Review policy fields

The review policy (reviews) decides which pull requests a review line takes. See Review tasks for how a line is chosen and the task it opens.

FieldTypeDefaultDescription
startstringmentionedmentioned: take a pull request when the mention handle appears in a comment or review on it. opened: also take every pull request as it opens.
mentionstring@spotlightThe handle that asks for a review. Matches ^@[A-Za-z0-9][A-Za-z0-9_-]*(\[bot\])?$. Matched as a whole word, without regard to case.
labelstringspotlightThe pull request label that asks for a review, whatever start is. 1 to 50 characters. Matched without regard to case.
skip.spotlightbooleantrueSkip pull requests Spotlight opened: a head branch under spotlight/, or a pull request a task opened.
skip.draftsbooleantrueSkip draft pull requests when they open. A mention or a label on a draft is still taken.
skip.authorsarray of strings[]GitHub logins whose pull requests are skipped, as in dependabot[bot]. Compared, stored and sorted in lower case, each once.

Replay check fields

The replay check (replayCheck, in Terraform replay_check) sets the threshold a planned change of one of the line's work stations is held to by spotlight replay check. A check reads it from the line as it is applied, never from the plan. A line read through the team's GET /api/teams/{teamId}/lines carries it as replayCheck, and a replay set's page shows it in the ⓘ of its "Checks" panel. See Replay checks API.

FieldTypeDefaultDescription
mostRegressionsnumber0How many tasks a planned change may regress on and still pass. A whole number, 0 or more. In Terraform, most_regressions.
ceilingnumber10The most each replay a check starts may spend, above 0 and at most 1,000: the planned change's replay, and the baseline replay when the guarding set has none for the live config. See Ceiling.
currencystringUSDThe three-letter currency ceiling is counted in. Stored in upper case.
minGainnumber0.05The smallest gain in success rate that counts as more than noise, as a share above 0 and at most 1. A gain over the baseline or the live config reads better only when its whole 95% interval is above 0 and it is at least this, and worse in the same way for a loss. It is read by the replays, searches and checks of the line's work stations; a line without replay_check reads them against 0.05. It does not decide whether a check passes. See Gains. In Terraform, min_gain.
MessagePath
Replay check mostRegressions must be a whole number from 0.replayCheck.mostRegressions
Replay check ceiling must be above 0 and at most 1,000.replayCheck.ceiling
Replay check minGain must be a share above 0 and at most 1.replayCheck.minGain
Replay check currency must be a three-letter currency code, as in USD.replayCheck.currency

Source fields

The source (source) names the file that holds this spotlight_line resource. A picked search candidate is opened as a pull request against it.

FieldTypeDefaultDescription
repositorystringrequiredThe GitHub repository, owner/name. Spotlight's GitHub App must be installed on it.
pathstringrequiredThe .tf file, from the repository's root, with forward slashes and no leading slash, as in teams/core/delivery.tf. 1 to 1,024 characters, with no empty, . or .. part.

For Spotlight to edit the station, the file must hold exactly one resource "spotlight_line" with the line's key written as a plain string, and in it exactly one station block with the station's key written the same way. An attribute a search may change that is read from a file must be read as file("${path.module}/<path>"); a plain file("<path>") cannot be followed. See The pull request.

MessagePath
Config source repository must be a GitHub repository as owner/name, not <repository>.source.repository
Config source path must be 1 to 1,024 characters.source.path
Config source path must be a path from the repository root with forward slashes, as in teams/core/delivery.tf.source.path
Config source path must not contain empty, . or .. parts.source.path
Config source path must end in .tf.source.path

Placement

A run at a station goes to a runner that carries at least one of the station's runners labels. A station's runners, when set, are the only labels that apply. A station without runners uses the line's. With neither, any runner of the workspace qualifies.

A label is one a runner pool or runner trust gives the runners it admits. A pool's key is not a label. Each entry, trimmed of surrounding whitespace, is shaped as a label is, lowercase words joined by dashes of at most 64 characters, and named once; any other entry is refused.

MessageCause
Runners must each name a pool or trust label in lowercase words joined by dashes, at most 64 characters, such as vpc-internal.An entry is not shaped as a label.
Runners must name <label> only once.An entry is given twice.

Each is returned with 422 at the path runners, or stations[<n>].runners for a station.

Where the line's repositories name an image, the runner must also run that image. Where they name none, the runner must run its host's default image. See Host settings for the images a host serves.

Layout rules

A line's stations are refused unless they follow every rule below.

  • A line must have at least one station.
  • Each station key must be used once.
  • A line must have exactly one backlog station, and it must be first.
  • A line must have exactly one completed station, and it must be last.
  • The completed station must not pass to any station.
  • A station must not pass to itself.
  • A station must pass only to work stations.
  • A reaction's to and from must be stations on the line.
  • A line must not have two reactions with the same happening and from.
  • If any station has both check and diffs: true, every repository must have setup.
  • A line must name reviews only if its role is review.
  • Only a work station may have a judge or alerts. On a backlog the refusal reads Line <line> station <station> is a backlog and has no runs to judge., and on the completed station Line <line> station <station> is the completed station and has no runs to judge.; for alerts both end and has no runs to alert on. <line> and <station> are keys. The refusal is returned at stations[<n>].judge or stations[<n>].alerts.

Reserved station keys

KeyA run that hands over to it
reviewhands its task over to a person
donefinishes its task

Update and delete

OperationBehaviour
updateReplaces the whole definition. A station whose key is kept keeps its identity and the tasks at it. Changing key is refused with 422. Dropping a station where any task stands is refused with 409 in-use, naming the stations and how many tasks stand at each.
deleteRefused with 409 in-use while any task stands on the line.

A second line claiming a role another line of the team holds is refused with 422 on role.

On this page