Subchapter 66.2
references/scout-data-model.mdMarkdown45 KBView on GitHub
Three records describe a scout’s life on a project, plus one snapshot it orients from.
This reference is the vocabulary for everything exploring-scouts returns.
One row per (team, skill_name).
Returned by scout-config-list.
This is the scout’s control surface, separate from its instruction body (the LLMSkill).
| Field | Meaning |
|---|---|
id | Config id: the handle scout-config-update takes to tune it. |
skill_name | The skill this config controls. Fixed; one config per skill per team. Usually signals-scout-*, but any skill name can hold a config. |
display_name | The name the UI shows. Blank means the default derived from skill_name. |
description | What the scout watches, read live from the skill’s frontmatter. Empty when the skill is gone from the team (an orphan config) or has no description. |
scout_origin / owners | canonical (PostHog seeded it) or custom (team-authored); owners lists the skill’s owners. The value comes from the skill row’s seed metadata, which survives an in-place edit, so a canonical scout the team has edited (diverged, no longer synced) still reads canonical here. |
enabled | false = paused. The coordinator skips disabled scouts entirely. Derived from status. |
status | Who owns the pause: active, pending_pause (still runs; an ignored warning pauses after a week, a no_output warning only ever warns; any config edit clears it), paused_by_system (automatic, resumable with enabled: true), paused_by_user (a person switched it off; the system never overrides it). |
pause_reason | Why the system paused or warned: no_output, ignored, or repeated_failures. Null outside pending_pause / paused_by_system. |
status_changed_at | When status last changed: for pending_pause, when the warning was issued (an ignored warning pauses about a week later; a no_output one never does). |
consecutive_failure_count | Current streak of failed scheduled runs (manual and workflow-triggered failures don’t count; a clean run from any trigger clears it). The failure breaker (repeated_failures) trips one failure past the runs the schedule fits in twelve hours, clamped to 5–25 (daily: 5, rolling hourly interval: 13, hourly cron: 15 because cron slots are counted over a DST-padded window); any config edit resets it. |
auto_pause_exempt | true = the inactivity sweep never judges this scout (for a watchdog whose silence is the job, or output consumed where the inbox can’t see it). |
emit | false = dry-run: the scout runs and reasons every tick but writes nothing to the inbox. |
run_interval_minutes | Rolling cadence, 30–43200. Default 1440 (daily). The coordinator dispatches when due. |
run_cron_schedule | Optional five-field cron in the project timezone. When set it takes precedence over the interval; a new or edited schedule waits for its next slot rather than catching up. |
output_destinations | Where reports go besides the inbox: slack (integration plus a channel or up to five DM targets, optional thread_reports) is the only destination Signals delivers to itself; a webhook entry is a pointer to a CDP destination the owning product manages, and says nothing about whether anything is delivered there. |
network_access | What the run’s sandbox can reach: trusted (default: the platform’s trusted-domain allowlist: PostHog, GitHub, package registries) or full (any site, for skills that read external docs/papers). |
mcp_gateway_server_ids | MCP store servers the scout’s runs may mount, from connections shared with the whole team. Empty = no shared servers (where gateway enforcement is not yet active, the acting user’s personal connections may still mount). Security-relevant like network_access, and activity-logged. |
write_scopes | Extra project-wide write scopes a person granted this scout (dashboard:write, insight:write, annotation:write, alert:write, llm_skill:write, warehouse_view:write, warehouse_table:write, replay_scanner:write). Empty = read-only beyond findings, memory, and notebooks. A dry run never holds them. |
model | Pinned model for this scout’s runs, or null to follow the fleet default. Setting one needs the scouts-model-config early-access flag on the project. |
structured_output_schema | Optional JSON Schema (object-rooted) describing one structured record the scout produces via scout-record-output. Null = the structured-output channel is off (it also needs emit on). When set, each run is shown the schema and its records are validated against it and recorded as $scout_structured_output events. |
tags | Free-form kebab-case labels (up to 10) grouping the fleet; scout-config-list filters on them. |
source_product / source_id | Set when another product stood the scout up for one of its own objects (Replay Vision creates one per scanner) so it can find and clean up its scouts; null for a scout a person created directly. |
last_run_at | When the coordinator last dispatched it. null = never dispatched by the coordinator, not never run: manual (scout-run-now) and workflow-triggered runs leave it untouched, so check scout-runs-list before calling a scout never-run. Drives the due-check (with run_cron_schedule and creation time on a cron scout). |
The replay_scanner:write grant permits scanner maintenance across the project.
Scouts must set a credit limit on scanners they create, copy, or enable.
Changes to targeting, sampling, or the model of an enabled scanner also require a limit.
Scouts cannot remove a limit, delete a scanner, or start manual scans, prompt tests, retries, or backfills.
They can disable a scanner and keep its observations.
Shared ratings must record explicit user verdicts. Autonomous assessments belong in scout memory or reports.
| updated_at | When the config last changed, including settings edits and system status changes. Scheduled dispatch updates last_run_at, not this field. |
A scout that is enabled: true, emit: false is alive and working — it just can’t post reports.
It is the extra-careful rollout posture for a chatty or high-stakes scout (the default is emit: true), and the most common cause of “my scout does nothing” reports.
Returned by scout-runs-list (summary) and scout-runs-retrieve (detail; same shape).
Each run is one sandboxed agent execution of one scout.
The run is a thin bridge to a tasks.TaskRun — status, timing, and the full transcript live on the Task side.
scout-runs-retrieve takes the run id as id, not run_id — even though the list and the detail payload both name the field run_id.
Pass the list’s run_id value through as id.
| Field | Meaning |
|---|---|
run_id | UUID of the run. Pass it to scout-runs-retrieve as id. Strictly team-scoped (404 across teams). |
skill_name | Which scout ran. |
skill_version | The body version that ran. If a scout was edited, older runs ran an older version — useful when comparing behavior before/after a change. |
status | Run outcome, from the linked TaskRun (see below). |
started_at | ISO-8601 — when the TaskRun was created. |
completed_at | ISO-8601 — when it finished. null while in flight. Duration = completed_at - started_at. |
emitted_report_ids | The reports the run authored via emit_report, in order. This (with edited_report_ids) is the run’s output — resolve each id via inbox-reports-retrieve. Empty = the run authored nothing, except on a measurement scout, whose output is its $scout_structured_output records. |
edited_report_ids | The reports the run edited via edit_report (title/summary rewrite, appended note, or reviewers set), deduped. Edits can target any inbox report, not just scout-authored ones. |
emitted_count | Legacy — how many weak findings the run emitted on the deprecated signal channel. Always 0 on report-channel scouts; populated only on runs from scouts still on the legacy channel. Don’t infer “did nothing” from 0 — check the report id columns. |
emitted_finding_ids | Legacy — the finding_ids behind emitted_count, in emit order. Each maps to a Signal with source_id = run:<run_id>:finding:<finding_id>. Empty on report-channel scouts. |
metadata | Two server-written regions. Top-level keys are runner-stamped at run start: harness_prompt_version / report_channel (none/emit/edit/both) / skill_origin / github_guidance (the provenance set — which prompt build, which report tools, canonical or custom skill, and whether the GitHub evidence section was rendered; segment on all four before comparing runs, and treat runs missing them as unknown rather than pooling them), plus model / runtime_adapter / reasoning_effort / service_tier only when routing overrode the default. metadata.derived is the harness’s own map of booleans computed at finalize (has_emit_report, has_edit_report, has_self_improvement, has_chart, has_self_validation, has_structured_output) — the structured answer to “what kind of run was this?”, so read it before parsing summary. Nothing here is scout-declared, so it can’t contradict the run’s actual output — except has_structured_output, which counts accepted batches rather than delivered events (see the structured-output section below). No derived map at all means unknown — the run predates the field (most historical runs), never reached finalize, or its stamp failed — never that every flag was false. |
task_id, task_run_id | Identifiers on the Tasks side. |
task_url | Relative deep-link to the Tasks UI for this run — the full transcript (every tool call and reasoning step) lives here, not in the run payload. |
summary | The scout’s own one-paragraph end-of-run close-out. The primary thing to read and relay. Empty for runs that errored before close-out. |
error | The full TaskRun error text on a run that didn’t complete cleanly; null otherwise. |
failure_reason | A concise, derived one-line diagnosis of why the run failed (timeout at the budget, a tool erroring, a sandbox that never started). Read this before the transcript. |
status flows from the linked tasks.TaskRun: not_started, queued, in_progress, completed, failed, cancelled.
Treat a completed run with an empty-handed summary as a healthy quiet run, not a failure — most runs should close out empty.
not_started / queued / in_progress: dispatched or running (completed_at null).completed: finished cleanly.
May or may not have written anything; check emitted_report_ids / edited_report_ids (both empty = quiet, unless it’s a measurement scout — then check metadata.derived.has_structured_output).failed: the run errored before closing out.
Its summary is empty; failure_reason says why in one line and error carries the full text, so the transcript (open task_url, or pull it as data with tasks-runs-session-logs-retrieve) is only needed for the sequence of calls that led there.
In practice the common failure is a timeout at the 15-minute per-run budget, not a logic-broken scout; a failed run whose duration ≈ the budget is a timeout, and failure_reason names it.
The usual cause is over-investigation (the scout ran to the wall), but some are false timeouts — the scout finished quickly and the run then hung on a dropped close-out; the session log distinguishes the two (tool calls up to the wall vs. silence long before it).cancelled: the sandbox activity was cancelled mid-flight (a worker shutdown or deploy). Says nothing about the scout, so it doesn’t count toward the failure breaker.Read the summary and completed_at together rather than keying on one status string.
The run row tells you exactly what it wrote: emitted_report_ids lists the reports the run authored (bumped post-success on each emit_report; preflight-skipped / dry-run writes don’t count) and edited_report_ids the reports it mutated.
Resolve any id via inbox-reports-retrieve to read the report itself.
Filter the list endpoint with emitted=true / emitted=false to slice runs by outcome without reading any prose — note emitted=true means “authored a report (or, legacy, emitted a finding)”; a run that only edited reads as emitted=false.
A run with both columns empty closed out empty — expected and correct most of the time, since scouts only write when they clear a high bar.
On a measurement scout the columns say nothing about output at all: it files no report on a normal run, so read metadata.derived.has_structured_output and the events themselves.
Each authored report’s backing evidence persists as signal rows tagged source_product="signals_scout", so inbox-reports-list { "source_product": "signals_scout" } is the direct way to list the reports the fleet has surfaced.
Runs from scouts still on the legacy signal channel (no allowed_tools opt-in — old custom scouts, or a canonical scout not yet ported) emit weak findings instead: emitted_count is that tally and emitted_finding_ids lists the finding_ids behind it.
Each finding went through emit_signal() with source_product="signals_scout" / source_type="cross_source_issue" and a deterministic source_id = run:<run_id>:finding:<finding_id> (stored at the top level of the signal’s metadata, not inside metadata.extra).
Grouping generated its own document_id and deduped on that — never on source_id — so a re-emitted finding_id produced a second signal.
For these runs only, scout-runs-emission-reports maps each emitted finding to the inbox report its signal grouped into (or null).
On report-channel scouts both fields are always 0 / empty.
One event per record a run submitted via scout-record-output, recorded only when the scout’s config carries a structured_output_schema (and emit is on — a dry-run scout has no channel).
The queryable view of a measuring scout’s output (judgments, scores, classifications), read like any events — trend them in insights or query the events table, filtering on the properties below.
| Property | Meaning |
|---|---|
run_id | The run that recorded it. |
skill_name | Which scout produced it. |
subject | Optional scout-chosen key naming what the record is about (a report id, URL, account key). Null for run-level. |
output | The full record, validated against the config’s schema at submission time. |
output_<key> | Each scalar top-level payload key, flattened for direct breakdowns (e.g. output_verdict). |
Event uuids are deterministic per (run, batch index, subject, payload), so a retried batch collapses at ingestion rather than double-counting.
A run’s metadata.derived.has_structured_output flag is a cheap per-run screen for “at least one batch was accepted”, without querying events.
It is not delivery confirmation: acceptance is counted before the events are forwarded, so a capture failure afterwards leaves the flag true with fewer or no events behind it.
Count the events themselves whenever the number of records matters.
Returned by scout-scratchpad-search.
One row per (team, key); re-using a key upserts.
This is the fleet’s cross-run memory — prose entries scouts write so future runs are smarter and quieter.
| Field | Meaning |
|---|---|
key | Agent-chosen semantic key, unique per team. Carries a category prefix. |
content | Prose, read verbatim into a future run’s prompt. |
created_by_run_id | Which run wrote it (null if the run was later deleted). |
created_by_skill | The original writer: a scout’s skill name, or pipeline:report-research / pipeline:implementation for a report-pipeline stage. An upsert keeps the creator, so the last writer can differ. |
created_at / updated_at | When written / last rewritten. |
expires_at | Optional. Expired entries drop out of searches (unless include_expired=true) and a daily janitor deletes them two weeks past expiry. |
The key prefix tells you the kind of learning: pattern: (baseline), watch: (a live issue tracked but still below the report bar), followup: (a validation probe a later run re-runs), noise: (ignore), addressed: (fixed/moved on), dedupe: (gate re-filing), allowlist: (never re-surface), not-in-use: (surface not used), mcp-gap: (tooling gap), report: (an authored report’s report_id), reviewer: (a cached owner for reviewer routing).
This vocabulary is open — scouts coin their own prefixes and <domain> labels, so treat an unfamiliar prefix as just another category.
Entries link to each other with [[key]] wikilinks.
The canonical prefix set and the four-state dedupe classifier the fleet reasons in terms of live in the authoring-scouts skill (references/dedupe-and-memory.md).
Returned by scout-project-profile-get.
A deterministic, cached snapshot of “what’s true about this project” — products in use, product intents, integrations, warehouse sources, signal source configs (split enabled/disabled), inbox report counts, and top events with reach/burst metrics.
This is the ground truth every scout cold-starts from.
When exploring, reach for the profile to explain scout behavior: a scout watching a surface the profile shows as absent (no logs, no LLM events, no revenue source) has nothing to do, and its quiet runs are correct. The profile is ground truth from authoritative tables; the scratchpad is the fleet’s inferred learnings — don’t conflate them.
Useful context when a scout’s runs are sparser than its schedule implies.
A periodic Temporal coordinator ticks (~every 30 min) and, for each enrolled team, dispatches every enabled scout whose schedule is due, most-overdue first, capped per tick.
On a rolling interval, due means last_run_at is None or now - last_run_at >= run_interval_minutes; on a run_cron_schedule, due means the first slot after the latest of last_run_at, the last schedule edit, and creation has passed, so a fresh cron scout waits for its first slot.
Enrollment is via the signals-scout feature flag’s allowlist.
So a scout can be enabled yet run late if: the team was drained from the flag, the scout was disabled, or busy ticks hit the per-tick cap.
There is no sampling — a due, enabled, enrolled scout runs.
Separately, a scout paused by the failure breaker (repeated_failures) gets one probe run a day and resumes on a clean one, unless the project is at its enabled-scout cap, in which case the row stays paused_by_system until another scout is paused or deleted (enabling it by hand is refused at the cap too).