Skip to content

Agent Output

Emit queryable core and approved specialized records from your agents, then triage and link them in the platform.

Agent Output is the structured side of what an agent produces — typed records it emits as it works, so an engagement’s results can be filtered, linked, and reported on instead of read line by line. The two core record types are finding and asset, and a capability can opt into approved specialized types.

Turn it on with one line in the manifest:

capability.yaml
outputs: true

That enables three mutation tools — report_item, update_item, and link_items (records are items in the tools and API) — plus the core finding and asset types. When the agent discovers something, it reports it:

report_item(
item_type="finding",
title="SQL injection in /login",
severity="high",
ref="sqli-login",
)
# Reported finding 'SQL injection in /login' (ref: sqli-login)

The record lands in the project, tied to the session and trace span that produced it. Mutation tools are opt-in: a capability with no outputs config cannot report, update, or link records. A platform-connected agent can still read the project’s existing output.

An API client can inspect published item types before a project contains any records. GET /api/v1/org/{org}/item-types returns the organization catalog, while the project-scoped /item-types endpoint adds the number of records the caller can read, including approved types with a count of zero. Type details include the JSON Schema, examples, emission guidance, labels, and rendering hints.

Each registry-backed record carries a stable definition ID and the exact immutable version ID used to interpret it. When a newer type version is published, use the project version endpoint to retrieve the historical contract instead of interpreting an older record with the current schema.

Each type’s fields are parameters on report_item, not a nested payload. The runtime builds the tool from the schemas your manifest selects, so the agent fills title and severity directly.

outputs in the manifest selects which record types the agent can emit:

outputsResult
true or { enabled: true }Built-in finding and asset
false, { enabled: false }, or omittedNo output mutation tools
findingOne selected platform type
[finding, asset, attack_surface]Selected core and specialized platform types
{ values: [...] } or { types: [...] }Selected platform types in legacy map form

Deprecated produces and items declarations still load and package with a warning. When outputs appears with either deprecated key, outputs takes precedence and the declarations do not merge.

A finding is something the agent discovered that matters; an asset is something it identified in scope. Both require a title; the rest of the payload is validated against the type’s schema and stored as queryable data. Core types drop unknown fields. A specialized contract decides whether it accepts extra fields through additionalProperties.

A finding carries severity and impact:

FieldTypeNotes
titlestrRequired. Short, specific.
severitycritical | high | medium | low | infoDefaults to info.
descriptionstrWhat was observed and why it matters.
categorystrCWE/OWASP bucket or domain category.
evidencestrRequest/response, command output, or a file path.
metadatadictMachine-readable context.

An asset describes something in scope and has no severity:

FieldTypeNotes
titlestrRequired.
asset_typestrhost, service, file, account, endpoint, model, artifact.
identifierstrHostname, IP, URL, path, account id, or artifact URI.
descriptionstrContext or provenance.
metadatadictMachine-readable context.

The platform renders description (on findings and assets) as Markdown: backticks become inline code, **bold** and *italic* are emphasized, and lists and fenced code blocks format as written. Three deliberate constraints, because a description is agent-authored and can be adversarial:

  • Links are never clickable. A [label](url) renders as label (url); a bare URL renders as the URL, as text. Neither can be clicked, which blocks in-product phishing. Literal HTML (an <img> or <script> in a quoted payload) shows as text rather than markup.
  • A link carrying an unsafe scheme loses its URL entirely. [click](javascript:...) renders as click alone. Put the payload you need to preserve in evidence, which the platform shows verbatim.
  • Prose whitespace collapses, per normal Markdown — runs of spaces and indentation are not preserved. For column-aligned scanner output or a pasted request/response, use a fenced code block, or evidence.

To address a record later — to update it or link it — give it a ref:

report_item(
item_type="asset",
title="web-1",
asset_type="host",
identifier="10.0.0.5",
ref="web-1",
)
# Reported asset 'web-1' (ref: web-1)

A ref is unique within the project for the life of the project, not per run, and reusing one fails the report. Namespace refs by run when a capability runs repeatedly against the same project — for example f"{run_id}-web-1" — or the second run’s first report errors and the record never lands.

When findings and assets don’t fit — an attack surface, a harvested credential, a model artifact — select an approved platform type by identifier:

capability.yaml
outputs: [finding, asset, attack_surface]

The attack_surface identifier must already have an active, approved contract in the platform registry. Publishing pins the capability artifact to that exact registry version; it never creates or activates a type. New registry versions never move the pin. Publish a new capability artifact to adopt the current contract. If the identifier is missing or inactive, publishing fails and asks a platform administrator to publish or activate it.

The connected runtime loads that pinned contract and hands the agent a report_item with the contract’s fields, descriptions, enums, defaults, and required fields. The agent provides host and open_ports directly, and both the runtime and platform validate the record against the same exact version:

report_item(
item_type="attack_surface",
title="10.0.0.5 — 2 ports open",
host="10.0.0.5",
open_ports=[22, 443],
)
# Reported attack_surface '10.0.0.5 — 2 ports open' (id: 6b1f0a9c-...)

Identifier-only specialized outputs require a platform connection when the runtime builds report_item. Without the pinned contract, the runtime fails closed instead of exposing an unvalidated payload. Legacy artifacts that embed a local Pydantic schema keep that local validation fallback during the compatibility window.

A platform administrator manages specialized contracts in Operations → Objects. SaaS catalogs belong to Dreadnode platform administrators; on-prem catalogs belong to local platform administrators. Organization owners and capability publishers can discover approved contracts but cannot import, publish, disable, or reactivate them.

An imported contract starts as a draft. Publishing creates an immutable version that new capability artifacts can pin. Deprecated contracts remain discoverable and existing pins can keep writing; disabled contracts reject new writes while historical records remain readable. Capability publication never creates or activates a missing contract.

During an upgrade, eligible records without registry references can still use their capability artifact schema and show a Legacy fallback marker. Operators monitor every fallback use and resolve migration conflicts before removing that compatibility path in a separately reviewed release.

Reports aren’t final. update_item patches a record or moves it through triage, and link_items connects records with a typed relationship:

update_item("web-1", status="verified", notes="Reachable, credentialed access confirmed.")
# Updated item web-1
link_items("sqli-login", "web-1", relationship="affects")
# Linked sqli-login -[affects]-> web-1

Triage status is one of open, triaged, verified, resolved, or dismissed. A finding that has never been triaged reports and displays as needs_review — a derived sixth state, not a value you set, and the one the platform counts and filters on out of the box. Notes alone never move it: attaching a rationale without naming a status leaves the finding in the review queue. Triage is also reversible — update_item("web-1", clear_status=True) removes the status and returns the finding to needs_review (over the raw API, send an explicit "status": null).

Status lives in a separate overlay from the record’s data, so triage state and the agent’s original report never overwrite each other.

Workspace contributors and owners can also edit these fields on the Agent Output page. Pick a status to apply it immediately, or edit notes inline and move focus away to save them. Each field updates independently; readers see the saved values without edit controls.

Give a connected agent a large codebase analysis and let it resume from structured records instead of replaying the entire transcript:

search_items(
"TokenVerifier",
item_type="finding",
status=["needs_review"],
include_facets=True,
limit=20,
)
# {"items": [{"ref": "auth-17", "title": "JWT bypass",
# "severity": "high", "effective_status": "needs_review"}],
# "next_page": 2, "facets": {"statuses": [{"value": "needs_review", "count": 18}]}}
read_item("auth-17")
# {"item": {"ref": "auth-17",
# "data": {"evidence": "Unsigned JWT accepted at src/auth/TokenVerifier.ts:418"}},
# "links": [{"relationship": "depends_on", "direction": "outgoing", ...}]}

list_items and search_items return compact summaries, filter-aware pagination, and an optional facet snapshot. Use next_page to continue a scan and read_item to load the complete payload for one result. Search covers titles, refs, types, capability names, and keys and scalar values inside structured data.

Search results default to PostgreSQL relevance ordering: title and ref matches rank above type and capability metadata, which rank above matches found only in structured data. Pass another sort value when recency, severity, or triage status matters more. read_item includes readable incoming and outgoing links by default; pass include_links=False when you only need the record.

The read tools belong to the connected project, not the capability manifest. The runtime asks the platform for the current credential’s effective grants and exposes them only when items:read is allowed; the API checks access again on every call. outputs controls whether an agent can mutate Agent Output, and explicit agent tool policy can still remove read tools.

A finished engagement leaves its records on the Agent Output page, under Agents → Output in the sidebar. Pick a project, then pick a record type from the selector in the toolbar. The selector lists every approved project type by its registry label, including types with no records yet. Record details use the exact contract version stored on that record, and deprecated or disabled types remain readable with their lifecycle status. During migration, eligible records without registry references show a Legacy fallback marker instead of appearing registry-backed. A single session’s records use the same labels under that session’s Output tab.

The project carries across the Agents area, so arriving from Sessions opens on the project you were already looking at — the one you filtered the session list to, or the one the open session belongs to — rather than resetting to the default.

If a shared link names a project that was deleted or does not exist, the page shows Project not found instead of loading indefinitely. Return to the workspace’s default project or open the project picker to choose another one.

Each type gets its own table. Types that carry severity open worst-first and lead with a band showing the worst severity present, the total, how many need review, and the critical-plus-high count. Severity and status filters sit in the table’s toolbar; assets have no severity, so that filter doesn’t appear for them. Sorting works on severity, status, and creation time.

Clicking a row opens a detail panel with the record’s fields, its evidence, its links, the session and trace span that produced it, and a changelog of the edits it has been through. Human changes name the operator who made them; agent changes retain their capability and run provenance. Single-field changes read as activity sentences, longer prose edits expand into a line diff, and multi-field operations stay grouped. History keeps an excerpt of very long values rather than the whole thing — when an edit falls outside that excerpt, the entry says so instead of appearing empty. From there you can copy the record as Markdown for a ticket, or copy a link to it. That link restores the record and its type; it does not carry your filters, search, sort, or page, so a recipient sees the same record against an unfiltered table.

A single session’s records also appear under that session’s Output tab, as cards linking back to this page.

Records inherit the visibility of the session that produced them: output from a private session is readable only by people who can see that session, while project-level records are visible to the whole project. Updates apply the same visibility rule, so a hidden record cannot be edited or returned through an update request.

Every table exports from its toolbar. The export runs against the server, not the rows on screen — you get every record matching the current type, filters, search, and sort order, across all pages, and only records you could already see.

The file carries a fixed set of columns rather than the ones the table displays: ref, item_type, title, severity, status, category, description, evidence, session_id, trace_id, created_at, and id. Showing or hiding a column in the table doesn’t change it. Fields outside that set don’t export, so a capability-defined type contributes its title and provenance but not its own fields.

Two things to expect in the file. status carries the derived needs_review for untriaged findings, so it round-trips back as a status filter. And any value starting with =, +, -, or @ gets a leading apostrophe, which stops a spreadsheet executing an agent-authored title as a formula — strip it if you’re diffing exports against another system.

Exports are capped at 10,000 records. Past that the export fails rather than handing you a truncated file that looks complete; narrow it with filters or search and run it again.

A rejected payload — a bad field, an unknown type, a reused ref — comes back to the agent as a tool error, so it can correct the call and retry. Anything else does not fail the agent’s turn: the tool reports the write as deferred and the run continues.

A reported record is written to the platform directly and logged to the agent’s trace at the same time. If the direct write fails — a transient network error, say — the record still lands in the trace as a backup, and the platform reconciles backups into the Agent Output page automatically when you freeze the session. A platform administrator can also force reconciliation for a run or session at any time. One caveat: records with very large payloads (over the SDK’s inline size limit) are offloaded to object storage and can’t yet be recovered this way.

Evaluation runs produce no records. The API keys minted for eval sandboxes carry no item-write access, so report_item from an evaluation is refused, and records tied to an evaluation session are excluded from every operational surface — this page, the session Output tab, the counts, the filters, and exports — so simulated work doesn’t mix with your operational agent output. Hosted optimization jobs run under the same restriction.

outputs is one of the capability manifest fields; the forms above are its full grammar. The read_item, list_items, search_items, report_item, update_item, and link_items tools — with complete parameter signatures — are documented in the dreadnode.tools reference.