Content Action Steps
For common fields, string substitutions, metadata filters, and execution order, see the Agent Steps Overview.
Retrieval Step
Searches a knowledge base using semantic similarity (vector search) to find the most relevant content for a given query. This is the foundation of RAG (Retrieval-Augmented Generation) workflows.
Fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
knowledge_base_id | UUID | Yes | — | The knowledge base to search |
query | string | No | null | The search query. Supports substitutions. If not set, the step's input is used; if that is blank too, the run's uploaded file is the query (see below). |
top_n | integer | No | 20 | Number of results to return (1–100). Prefilled from the knowledge base's default when you first pick it in the editor. |
reranker_model | enum | No | null | Reranker model to apply after initial retrieval. null inherits the knowledge base's reranker; "none" turns reranking off (see below) |
top_k | integer | No | null | Number of results to keep after reranking (1–100). Prefilled from the knowledge base. Ignored when reranking is off; still applied if Seclai skips one (below). |
minimum_rerank_score | number | No | null | Drop matches the reranker scored below this (0.0–1.0). Only applies when a reranker runs; never filters on similarity. Prefilled from the knowledge base. |
content_type | enum | No | application/json | Output format: application/json or text/plain |
filter | object | No | null | Metadata filter to narrow results (MongoDB-style query, see Metadata Filters) |
added_after | string | No | null | Only return content added after this date/time. Supports substitutions. |
added_before | string | No | null | Only return content added before this date/time. Supports substitutions. |
include_attachments | boolean | No | true | When enabled, indexed content attachments are included in retrieval results alongside the main content body. Disable to limit results to original content only. |
metadata_fields | string[] | No | null | Metadata keys to return on each match, read back from the metadata attached when the content was indexed. Omit to return none. |
Searching with a photo
A knowledge base indexed with a multi-modal embedding model can be searched with a file rather than a sentence — send a phone photo and get back the page, product or diagram it matches. Point the run at the file and leave the retrieval step's query blank:
- No
queryon the step, and - nothing feeding text into the step (the run's own input is empty, or the previous step produced nothing),
- while the run carries an uploaded file that is a media type — an image, a video, an audio file or a PDF.
The file then becomes the query. With several media files on the run, Seclai picks the first one your knowledge base's embedding model can actually read — so attaching a voice memo alongside a photo, on an image knowledge base, still searches with the photo. When none of them qualifies, the first is used and the fallback below applies. Files that are not media — JSON, an archive, a spreadsheet, a text file — are never used as a query; their extracted text goes through the ordinary query path instead.
To search with each of several files, put the retrieval step inside a for_each loop over attachments: inside the loop body the query is that iteration's own file, not the run's first one.
Which media types are embedded natively is decided by the knowledge base's embedding model, not by this step. Amazon Nova 2 Multimodal reads images, video, audio and PDF; Cohere Embed v4 reads images. A type your embedder cannot read searches by the file's extracted text instead (below).
The same applies when content is indexed: on a capable embedder, an uploaded image, video, audio file or PDF is embedded whole, in addition to being indexed from its extracted text — so a photograph is findable by a photo even though it has no words in it.
Five things follow from this:
- A file's own
# {filename}heading does not count as input. Every upload's extracted text is placed under a# {filename}heading, so a run carrying nothing but a photo still has{{agent.input}}equal to"# photo.jpg". That heading is a label Seclai writes, not something you typed, so the retrieval step ignores it when deciding whether the step has a query — otherwise every photo-only run would silently search the knowledge base for its own filename. Headings of files that carry real extracted text still count, along with that text. - A blank query is only an error when there is nothing else to search with. If the step has no query, no input, and the run carries no media file, the run fails with "Query is required" — and the message names the attachment types that were present, so an attached
.jsonthat you expected to work says so rather than looking like a missing query. - Text and a file can be combined — but not every embedder uses both. Send
inputalongside the upload and the text is offered as the query with the file attached to it. Cohere Embed v4 embeds the two together; Amazon Nova 2 Multimodal takes exactly one input per call and uses the file, discarding the text. Don't rely on the wording to narrow a media search unless your knowledge base is on an embedder that combines them. - The knowledge base has to be able to read the file. If the sources are embedded with a model that can't read the file's type, Seclai does not embed it: it drops the file and searches on text alone, saying so in the trace — "Searched text-only — embedder does not support the attached query media". Which text depends on what you sent. Text of your own is used as-is, with the file set aside. With no text, the file's extracted-text counterpart (PDF text, an audio/video transcript) becomes the query instead — and a file with no counterpart either, such as a raw photo, leaves nothing to search with, so the step fails and tells you to re-index with a multi-modal embedder. See knowledge bases.
- A media query is billed like a media chunk. Searching with a file costs one media unit at your embedder's rate for that modality — the same unit indexing charges per image — plus the tokens of any text you sent alongside it. Both appear as separate lines on the usage report.
The trace tells you which path a run took: "Searching knowledge base with the uploaded image" for a media-only query, "…with text and media" when both were sent, and plain "Searching knowledge base" for an ordinary text search. Reranking is skipped for a media-only query, since a reranker scores query text against match text, and there is no query text — see below.
The Test button in the step editor cannot preview this: a preview has no run and therefore no file, so it runs text retrieval only.
Two entrances to a knowledge base accept a file as the query, and they behave the same way because they are the same code path. This step is one. The other is knowledge base chat, where the paperclip attaches an image, audio file, video or PDF to your message: chat runs a retrieval step of its own and hands your uploads to it, so everything on this page applies there too. One difference — a chat message can never be empty, so a chat search is always text and file, never the file alone. Which of the two the embedder actually uses is the "text and a file can be combined" rule above.
The remaining ways in are text-only: the search_knowledge_base tool on a Prompt Call and the search MCP tools take a query string with nowhere to put a file. They still reach media — that is the indexing side, not the query side — and a media chunk they match is labelled with the same [image: …] citation described below.
Reranker Models
Rerankers improve result quality by re-scoring the initial retrieval results using a cross-encoder model. The retrieval first fetches top_n results, then the reranker re-scores them and returns the top top_k.
| Model | Description |
|---|---|
| Amazon Rerank v1 | AWS Bedrock-hosted reranker |
| Cohere Reranker v3.5 | High-quality reranker from Cohere |
The step's reranker_model has three settings:
| Value | Meaning |
|---|---|
null (unset) | Use whichever reranker the knowledge base is configured with — the default |
"none" | No reranking for this step, whatever the knowledge base is set to |
| A model ID | Rerank with that model |
Reranking and media knowledge bases
Rerankers are text models: they score your query text against each match's chunk text. A knowledge base whose sources are embedded natively as images, audio or video stores those chunks with no text at all — the binary is the content — so there is nothing for a reranker to score.
Seclai handles this for you in three places, so you generally don't have to think about it:
- A knowledge base created with every source using an embedding model that cannot handle text at all gets no reranker by default. (A multi-modal model — the default — indexes both, so it keeps one.)
- A retrieval whose query is an image (or other media) with no text skips reranking — there would be no query text to score with either.
- Within a mixed knowledge base, reranking only reorders the matches that actually carry text; native media matches keep the positions their similarity scores earned.
When Seclai skips a reranker your step or knowledge base had configured — including when the reranker itself fails — your top_k still bounds the results, and the run's trace says the reranking was skipped and why. Skipping does not quietly hand the next step top_n matches instead. A minimum_rerank_score keeps matches that carry no text for a reranker to score, since a threshold on rerank scores has nothing to say about a chunk that was never judged — otherwise it would quietly become a "text only" filter.
Set reranker_model to "none" when you want to be explicit — for example on a knowledge base that already holds a reranker from before its sources became media-only. That also skips the reranking charge, which the automatic skips above do too.
Output Format
When content_type is application/json (default), the output is a JSON object with a matches array of matching documents. Each object in matches contains:
| Field | Type | Description |
|---|---|---|
content_title | string | null | Title of the matched content, capped at 256 characters and cut with a trailing … beyond that, with line breaks flattened to spaces. Where it comes from depends on the source: a crawled page's own <title>, an RSS entry's title, or — for an upload — the title in the metadata you supplied, falling back to the filename. null when the content has no title. |
source_type | string | Source type (e.g. rss_feed, website, custom_index) |
source | string | Name of the source connection |
content_url | string | Absolute URL to the content viewer in the Seclai app |
external_url | string (optional) | Original source URL for website and RSS feed content. Omitted for content stores and other source types where not present. |
content | string | The matched text chunk. For a media match — a chunk that is an image, audio file, video or PDF — there is no text of its own: an asset extracted from a page or document carries its label instead (an image's alt text, a video's title or aria-label, a linked document's anchor text — capped at 512 characters and cut with a trailing …), and a match with no label carries the [image: name] citation, never an empty string (see below). |
chunk_start | integer | Character offset where the chunk starts in the full content. Both offsets are 0 on a media match, which has no position in a text body. |
chunk_end | integer | Character offset where the chunk ends in the full content |
content_publish_date | string | null | Original publish date of the content, if available |
page_number | integer (optional) | Page the match sat on, for an image extracted from a PDF. Omitted for text chunks and for formats without pages. |
source_url | string (optional) | URL the matched asset itself was fetched from, for an image extracted from a web page. Omitted for uploaded documents. |
source_mime | string (optional) | MIME type of the matched binary, for a media chunk. Omitted for text chunks. |
content_metadata | object (optional) | The metadata keys named in metadata_fields, for those the matched content actually carries. Omitted entirely when metadata_fields is not set. |
Example JSON output:
{
"matches": [
{
"content_title": "Getting Started Guide",
"source_type": "website",
"source": "Documentation Site",
"content_url": "https://app.seclai.com/app/abc123/contents/def456?start=0&end=1200",
"external_url": "https://docs.example.com/getting-started",
"content": "Welcome to the getting started guide...",
"chunk_start": 0,
"chunk_end": 1200,
"content_publish_date": "2026-01-15T10:30:00Z"
}
]
}
external_url is only present for website and rss_feed source types. For custom_index and other source types, the field is omitted.
page_number and source_url appear only when the match is an image (or other media) that was extracted out of a container document — a knowledge base with media indexing enabled. A PDF match carries page_number; an image pulled from a web page carries source_url pointing at the image itself, while external_url points at the page that contained it. Text chunks carry neither.
When content_type is text/plain, the output is the concatenated text of matching documents, suitable for direct use in prompts.
What a media match looks like
A knowledge base that indexes media returns matches whose content is a binary rather than text. Rather than handing the next step a blank content, Seclai fills that field one of two ways. An asset pulled out of a page or a document carries the label its author gave it — the alt text of an <img>, the title or aria-label of a <video>, the wording of the link a document was fetched from. Anything with no label of its own carries the same citation label the model is shown beside the file. That second case covers an uploaded file embedded whole, an image extracted from a PDF (which has no alt attribute to take), and equally a <video> or linked document whose own label was missing or blank:
{
"matches": [
{
"content_title": "Assembly Manual",
"source_type": "custom_index",
"source": "Product Manuals",
"content_url": "https://app.seclai.com/app/abc123/contents/def456?start=0&end=0",
"content": "[image: page_7_image_0.png (Find page, page 7, match 1)]",
"chunk_start": 0,
"chunk_end": 0,
"source_mime": "image/png",
"page_number": 7
}
]
}
source_mime is the field to branch on: it is present exactly when the match is a media chunk, and absent on every text chunk. Don't branch on an empty content — it is never empty on a media match, and a text chunk that produced no text looks identical.
A citation in content, as in the example above, is there so a text-only model still knows an image matched, and so a citation the model gives you can be traced back to a row in this output. It is built from the asset's own filename where it has one, falling back to the document title, and carries the step name, page and match position — see what the model sees. Don't parse it for meaning.
A label in content is the opposite: it is the caption the indexed page wrote for that asset, so it does carry meaning — but it is page content, not something Seclai composed, and it deserves the same trust you give the rest of a scraped page. It reaches you capped at 512 characters, with line breaks flattened to spaces and square brackets rewritten as parentheses, so a label can never be mistaken for one of the citations above.
The picture itself is not in this JSON. Matched media is added to the run's attachment pool, so a downstream LLM-call step (prompt_call, evaluate_step, extract_data) can receive the actual file and not just its label. Which reference you write decides whether it does:
| The prompt references | The model receives |
|---|---|
{{input}} | the matches and their files — the ordinary retrieval → prompt chain, and the reason this usually needs no thought |
{{attachments[*.png]}} | exactly the files that reference selects |
{{step.<id>.output}} only | the matches as text only — no files |
| nothing | no files |
The third row is the one that surprises people. A named reference to a step's output reads that step's own output manifest, and a retrieval step emits JSON or plain text rather than a manifest — so there is nothing there to attach. This is the normal shape when a prompt pulls from a retrieval on another branch, or from two retrievals at once. Add an explicit {{attachments[…]}} alongside it and the files arrive: the pool is run-wide, so it reaches matches from any retrieval that has already run, not only the step feeding this one.
Whatever route the file takes, the receiving step's model has to be able to read it. If it cannot, the file is replaced by its extracted text — nothing at all for a raw photo — so pick a vision model for the step that consumes image matches.
Reading Metadata Back
Content indexed through POST /sources/{id}/upload can carry an arbitrary metadata object, and a retrieval step can already filter on it. To also read it back, name the keys in metadata_fields:
{ "metadata_fields": ["page", "section"] }
Each match then carries a content_metadata object holding just those keys, and only the ones that content actually has. Keys you do not name are never returned — the same metadata also holds bulky derived values (a podcast transcript, every section anchor on a docs page, a full RSS description) and internal bookkeeping, none of which belongs in an agent's prompt. For the same reason a single very large value is omitted even when named, and the object as a whole is capped — a retrieval step can return up to 100 matches into one prompt, so metadata that is individually reasonable can still be too much in aggregate. Keep metadata_fields to the handful of keys the agent actually reasons about.
Three things worth knowing:
- The shape depends on
content_type. Withapplication/json(andapplication/xml, which emits JSON) each match carries acontent_metadataobject. Withtext/plainandtext/htmleach match instead gains a singleMetadata: page=7; section=Appendix Bline — same keys, same order, rendered askey=valuepairs joined with;(non-string values render as JSON). - Filtering and reading are not the same set.
filtermatches against the metadata stored on the content itself, while what you read back is that merged with any metadata awrite_metadatastep has since attached, with the step's values winning. So a key written bywrite_metadatais readable but not filterable, and if both set the same key you filter on the original value while reading the newer one. content_publish_datekeeps its own top-level field even if you also namepublished_at.
Use Case Examples
Basic semantic search:
- Knowledge base: Product documentation
- Query:
{{agent.input}} - Top N: 10
- Content type:
text/plain
The step searches for content semantically similar to the user's input and returns the top 10 matches as plain text.
Filtered retrieval with reranking:
- Knowledge base: News articles
- Query:
{{agent.input}} - Filter:
{"category": {"$eq": "{{metadata.category}}"}} - Top N: 50
- Reranker: Cohere v3.5
- Top K: 10
- Added after:
{{metadata.start_date}}
First retrieves 50 candidates matching the category filter added after the start date, then reranks them to select the 10 most relevant.
Time-bounded retrieval:
- Knowledge base: RSS feed content
- Query:
Latest news about AI - Added after:
2026-02-10 - Added before:
2026-02-17 - Top N: 20
Retrieves only content added within the specified date range.
AI Assistant
The retrieval step includes an AI assistant that can generate a complete configuration from a natural-language description. Click the AI Assistant button to open the assistant modal.
The assistant can set all retrieval fields:
- Knowledge base — selects the most appropriate knowledge base when none is chosen, or suggests switching when another KB better matches the request
- Query — writes query templates using substitution variables like
{{input}}and{{step.<id>.output}} - Top N / Top K — sets retrieval and reranking counts based on use case
- Reranker model — recommends a reranker when high relevance or multilingual content is needed
- Time range — sets
added_after/added_beforefilters using date templates - Metadata filter — builds MongoDB-style filter objects using known metadata fields from the knowledge base
- Content type — selects the output format (JSON, plain text, HTML, XML)
The assistant is aware of the available knowledge bases (their names, descriptions, source counts, and detected metadata fields), the available reranker models, and the full agent workflow context. When no knowledge base is selected, the assistant will recommend one based on your request.
Write Metadata Step
Writes a value to the content's metadata by key. This enables agents to persist structured information — such as AI-generated classifications, tags, or scores — directly on the content record so that it can be used for retrieval filtering and gate conditions in future agent runs. For larger information like summaries, consider using Write Content Attachment and Read Content Attachment steps instead.
Fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
metadata_key | string | Yes | — | The key under which the value will be stored in the content metadata. Supports substitutions. |
content | string | No | null | The value to write. If omitted, the step's input is used (equivalent to {{input}}). Supports substitutions. |
Behavior
- If the value is valid JSON, it is stored as a parsed JSON value (object, array, number, boolean, or null). Otherwise it is stored as a plain string.
- The total serialized metadata payload must not exceed 10 KB. Performance may degrade with payloads above 2 KB.
- Metadata written by this step is merged with content version metadata when content is retrieved. The content metadata takes precedence over content version metadata for duplicate keys.
- The step requires a
source_connection_content_version_idin the run metadata — this is automatically provided when the agent is triggered by a content event (content_added,content_updated, orcontent_added_or_updated).
Use Case Examples
Persist an AI-generated category for retrieval filtering:
Step 1: Extract Data — Classify the content into a category
Step 2: Write Metadata (metadata_key: "category")
A subsequent retrieval step can filter by {"category": {"$eq": "technology"}} to narrow results.
Store a sentiment score for gate conditions:
Step 1: Prompt Call — "Rate the sentiment of this content as positive, negative, or neutral"
Step 2: Write Metadata (metadata_key: "sentiment")
A gate step in a later agent can use metadata.sentiment with $eq to route content conditionally.
Write a JSON summary object:
Step 1: Extract Data — Extract key topics and summary as JSON
Step 2: Write Metadata (metadata_key: "analysis", content: "{{step.extract-data.output}}")
The JSON object is stored as structured metadata, and individual fields can be accessed in filters.
Write Content Attachment Step
Writes an attachment to content. The step input (or explicit content) is stored as a file-backed attachment under a specified key. This is ideal for persisting large outputs — such as full translations, analysis reports, or transformed content — that are too large for metadata but should be associated with the content record.
Fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
attachment_key | string | Yes | — | A short identifier for the attachment (e.g. summary, translation). Must be unique per content version. Supports substitutions. |
content_type | enum | No | text/plain | The MIME type of the attachment: text/plain, text/html, application/json, or application/xml. |
content | string | No | null | The content to write. If omitted, the step's input is used (equivalent to {{input}}). Supports substitutions. |
indexed | boolean | No | false | When enabled, the attachment text is indexed so that it appears in retrieval search results alongside the main content body. |
attachments | list | No | null | Optional list of {{…}} attachment references — when the step receives a multi-asset manifest input, the unioned matches must narrow to exactly one attachment (the step writes a single attachment per call). 0 or >1 match raises a validation error. |
Behavior
- Attachments are stored in file storage and linked to the content version.
- Unlike metadata (which has a 10 KB limit), attachments can store large content.
- When
indexedis enabled, the attachment's text is chunked and embedded into the knowledge base's vector store, making it searchable via retrieval steps. The retrieval step'sinclude_attachmentsfield controls whether indexed attachments appear in results. - The step requires a
source_connection_content_version_idin the run metadata. - Multi-asset manifest input (an upstream prompt step that emitted multiple images / files):
attachmentsmust narrow to one attachment. To write all attachments, wrap this step in a for_each withiterate_attachments=true.
Use Case Examples
Store a full translation alongside original content:
Step 1: Prompt Call — Translate the content to Spanish
Step 2: Write Content Attachment (attachment_key: "translation_es", content_type: text/plain)
Persist an indexed analysis that's searchable:
Step 1: Extract Data — Generate a detailed analysis report
Step 2: Write Content Attachment (attachment_key: "analysis", indexed: true)
The analysis is now searchable via retrieval, appearing alongside the original content in results.
Store structured data as a JSON attachment:
Step 1: Extract Data — Extract entities and relationships as JSON
Step 2: Write Content Attachment (attachment_key: "entities", content_type: application/json)
Load Content Step
Loads the full text body of a source document (content version) and returns it as the step output. For content-triggered agents, the triggering content is used automatically unless an explicit content version is selected.
Fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
content_version_id | string | No | — | The ID of the content version to load. If omitted, the triggering content version is used automatically. |
Behavior
- Returns the full text body of the content version as the step output.
- For content-triggered agents, the triggering content version is used when
content_version_idis not specified. - When an explicit
content_version_idis provided, that content version is loaded regardless of trigger context. - The step requires a
source_connection_content_version_idin the run metadata (provided automatically for content-triggered agents).
Use Case Examples
Load and summarize article content:
Step 1: Load Content
Step 2: Prompt Call — Summarize the following article: {{input}}
Load content for multi-step analysis:
Step 1: Load Content
Step 2: Extract Data — Analyze the content for key themes and sentiment
Step 3: Extract JSON — Parse the structured analysis
Load Content Attachment Step
Loads a previously written attachment from content by key and returns its text content as the step output. Use this to recall persisted content — such as a previous analysis, translation, or extracted data — for use in subsequent processing steps.
Fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
attachment_key | string | Yes | — | The key of the attachment to load. Supports substitutions. |
Behavior
- Returns the text content of the attachment as the step output.
- If the attachment does not exist for the given key, the step output is empty.
- The step requires a
source_connection_content_version_idin the run metadata.
Use Case Examples
Load a previous translation for comparison:
Step 1: Load Content Attachment (attachment_key: "translation_es")
Step 2: Prompt Call — Compare this translation with the original content
Build on a previous analysis:
Step 1: Load Content Attachment (attachment_key: "analysis")
Step 2: Extract Data — Using the previous analysis, identify any changes
Load Content Step vs Load Content Attachment vs Prompt-Call Tools
These three approaches all load document content, but serve different purposes:
| Approach | What it loads | When to use it |
|---|---|---|
| Load Content step | The full text body of a source document | You need the entire document as a deterministic pipeline step — e.g. to feed it into a Prompt Call or Extract Data step for summarization or analysis. |
| Load Content Attachment step | A previously written attachment (by key) | You need to recall data that was persisted by a prior agent run — e.g. a cached translation, extracted entities, or a previous analysis. |
load_content prompt-call tool | The full text of a source document, invoked by the AI model | You want the model to decide at runtime whether and which document to load. The model autonomously calls this tool during a Prompt Call step when it determines it needs document content. |
The key distinction: the Load Content step runs unconditionally as part of the pipeline, while the load_content prompt-call tool is invoked by the AI model on demand during a Prompt Call step. If you always need the document, use the step. If the model should decide whether to fetch it, enable the tool.
Combining Metadata and Attachments
Metadata and attachments serve complementary purposes:
| Feature | Metadata (Write Metadata) | Attachments (Write/Load) |
|---|---|---|
| Size limit | 10 KB total | No practical limit |
| Filterable | Yes (retrieval filters, gates) | No (but can be indexed for search) |
| Searchable | No | Yes (when indexed: true) |
| Best for | Tags, scores, categories | Full text, reports, translations |
| Access method | {{metadata.<key>}} | Load Content Attachment step |
Example: Extract Data-driven content enrichment pipeline:
Step 1: Extract Data — Classify content and extract summary
Step 2: Extract JSON — Parse the JSON output
Step 3: Write Metadata (metadata_key: "category", content: "{{step.extract-json.output.category}}")
Step 4: Write Metadata (metadata_key: "sentiment", content: "{{step.extract-json.output.sentiment}}")
Step 5: Write Content Attachment (attachment_key: "detailed_summary", indexed: true)
This pipeline: classifies content (filterable via metadata), records sentiment (usable in gate conditions), and persists a searchable detailed summary (retrievable via attachment indexing).
Publish Content Step
Publishes content to a Content Store source connection. The content goes through the standard indexing pipeline and becomes searchable via retrieval. Use this to let agents create or update knowledge base content — for example, storing generated summaries, reports, or transformed data.
Fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
source_connection_id | string | No | null | The ID of the target content store source. RSS feeds, websites, and other source types are not supported. For content-triggered agents, leave blank to publish back to the triggering content's source. Supports substitutions. |
content_identifier_mode | enum | No | auto | auto — creates new content each run. custom — uses a custom identifier so the same item is updated on subsequent runs. |
content_identifier | string | No | null | A custom content identifier (only used when mode is custom). Content with the same identifier is treated as the same item. Supports substitutions. |
if_exists | enum | No | new_version | What to do when content with the same identifier already exists: new_version (update) or skip (leave unchanged). |
content_type | enum | No | text/plain | The MIME type of the content: text/plain, text/html, application/json, or application/xml. |
title | string | No | null | An optional title for the published content. Supports substitutions. |
content | string | No | null | The content to publish. If omitted, defaults to the step input. Supports substitutions. |
metadata | object | No | null | Optional key-value metadata to attach to the published content. String values that look like substitution placeholders are resolved. |
suppress_triggers | boolean | No | true | When enabled, the published content will not fire downstream content triggers. Enabled by default to prevent infinite loops. |
attachments | list | No | null | Optional list of {{…}} attachment references — when the step receives a multi-asset manifest input, the unioned matches must narrow to exactly one attachment to publish as the content body. 0 or >1 match raises a validation error. |
Behavior
- Only Content Store sources are supported as targets. Other source types (RSS feeds, websites) will be rejected.
- In
automode, every run creates a new content item with a unique identifier. - In
custommode, publishing with the same identifier updates the existing item (creating a new version) or skips it, depending on theif_existssetting. - The
suppress_triggersflag defaults totrueto prevent infinite loops when a content-triggered agent publishes back to a monitored source. Only disable this if you are certain the target is not monitored by content triggers. - Works with any agent type. For content-triggered agents, the source connection and content version can be inherited from the trigger context.
- Multi-asset manifest input:
attachmentsmust narrow to one attachment. To publish all attachments as separate content items, wrap this step in a for_each withiterate_attachments=true.
Use Case Examples
Generate and store article summaries:
Step 1: Load Content
Step 2: Prompt Call — Summarize the article
Step 3: Publish Content (source_connection_id: "<custom-index-id>", title: "Summary: {{metadata.title}}")
Overwrite a daily report:
Step 1: Retrieval — Search for today's data
Step 2: Prompt Call — Generate daily report
Step 3: Publish Content (content_identifier_mode: custom, content_identifier: "daily-report", if_exists: new_version)
Content-triggered enrichment (publish back to same source):
Step 1: Load Content
Step 2: Extract Data — Analyze and enrich the content
Step 3: Publish Content (suppress_triggers: true)
Related pages:
- Content Sources — Configure the sources that feed your knowledge bases
- Contents — Inspect, replace, and delete indexed content items via API
- Knowledge Bases — Organize sources into searchable collections
Attachments references field (Publish / Write Content Attachment)
New to attachments? See the Attachments guide for the high-level model and examples.
Both Publish Content and Write Content Attachment write a single content body per call. When the step receives a multi-asset manifest input (e.g. from an upstream prompt step that emitted multiple generated images), the attachments field must narrow the manifest to exactly one attachment. To write every attachment, wrap the step in a for_each with iterate_attachments=true to fan out.
The attachments field is a list of {{…}} attachment reference expressions, the same grammar used by the Webhook, S3, Send Email, and Call Agent steps:
["{{attachments[0]}}"]
| Configuration | Behaviour |
|---|---|
null / [] with non-manifest input | Writes the step input as-is |
null / [] with manifest input | Validation error — the step needs explicit references when input is a manifest |
["{{attachments[0]}}"] | Writes the first input attachment |
["{{attachments[invoice.pdf]}}"] | Writes the attachment named exactly invoice.pdf |
["{{attachments[*.pdf]}}"] | Resolves to all PDFs — must match exactly one, else validation error |
["{{step.gen-image.attachments[0]}}"] | Writes the first image emitted by a prior step's output manifest |
For writing all attachments as separate content items, wrap the step in a for_each with iterate_attachments=true — the loop iterates one body execution per attachment.