Build workflows
Artifacts
Markdown documents that outlive the run that wrote them — reports, logs and briefs your workflows keep up to date, with every version kept.
A step's output lasts as long as its run, and a workspace is deleted the moment the run finishes. Artifacts are for what should stay: a weekly report, a running incident log, a brief a person reads in the morning. Each one is a markdown document that belongs to the account, has a slug such as ops-weekly, and keeps every version it has ever had.
The artifact tools are available to every account with nothing to set up, under the artifact. prefix, in tool steps and in an agent's tools list.
Writing one
steps:
- key: incidents
kind: tool
tool: postgres.query
args: { sql: "select title, severity, resolved_at from incidents where opened_at > now() - interval '7 days'" }
- key: report
kind: llm
prompt: |
Write this week's operations report in markdown, with a section per severity.
{{ steps.incidents.output }}
- key: save
kind: tool
tool: artifact.write
args: { slug: ops-weekly, title: Operations weekly, body: "{{ steps.report.output.text }}" }
write replaces the whole document and creates it if the slug is new. The next run writes the same slug again, and the old report is still there in the artifact's history.
Changing part of one
Rewriting a long document to change one line wastes tokens and invites mistakes, so the tools can also work on a part:
appendadds markdown to the end, or to the end of one section. A list item appended under a list stays part of that list.edit_sectionreplaces what is under one heading, up to the next heading of the same or a higher level, and leaves the rest alone.readwithsectionreturns only that section, and always lists the document's headings.
A section named in append or edit_section that does not exist yet is added at the end.
- key: log
kind: tool
tool: artifact.append
args:
slug: deploy-log
section: "{{ trigger.repository.name }}"
content: "- {{ trigger.head_commit.id }} deployed by {{ trigger.pusher.name }}"
Reading one in a template
Any step can read an artifact through a template, with no tool call: {{ artifacts.<slug>.body }}, and also .title, .version, .url and .updated_at.
steps:
- key: brief
kind: llm
prompt: |
Here is the brief as it stands. Rewrite the Today section for {{ trigger.scheduled_at }}.
{{ artifacts.morning-brief.body }}
- key: post
kind: tool
tool: slack.post_message
args: { channel: C0123, text: "Brief updated: {{ artifacts.morning-brief.url }}" }
A step reads the artifact as it is when the step starts, and loads only the artifacts it names. An artifact that doesn't exist resolves to nothing, so a condition can check for it:
if: { path: artifacts.morning-brief, exists: false }
Steps that write an artifact aren't linked automatically to steps that read it through a template. When one step of a run writes an artifact and a later step reads it, add the writer to the reader's depends_on.
From a workspace
When a coding agent or a script writes the document, artifact.import copies a text file out of the run's workspace into an artifact as its new body.
History and retries
Every change is a new version, recorded with the run and step that made it. The Artifacts page shows each document rendered, its history, and the diff between any version and the one before.
A step that is retried after an interruption does not apply its change twice: repeating the same call from the same step is recognised, and the version it already saved is returned.
Over MCP
Your own agents can use the same tools over MCP as artifact__list, artifact__read, artifact__write, artifact__append and artifact__edit_section. Changes made this way are recorded against the person the token belongs to.