Skip to Content
Tools📦 Artifacts and Code Execution

Artifacts and Code Execution

DocsGPT can generate documents, run code, and read files for you during a chat or inside an agent workflow. Three built-in tools work together for this:

  • Artifact builds and edits documents such as slide decks, Word documents, spreadsheets, PDFs, and HTML.
  • Code Executor runs code in a sandboxed session and turns any files the code writes into downloadable artifacts.
  • Read Document parses an uploaded or produced file (PDF, Word, PowerPoint, and more) into text, markdown, or structured data.

All three are opt-in: enable Artifact and Code Executor per agent in the tool picker (they need a running sandbox; see Code Execution Sandbox), and Read Document is available inside workflows. A new agent starts with no tools enabled.

If your deployment runs a sandbox and you want these available in every chat rather than per agent, add them to DEFAULT_CHAT_TOOLS:

DEFAULT_CHAT_TOOLS=["memory","read_webpage","scheduler","monitor","code_executor","artifact_generator"]

They are left out of the shipped default deliberately: both execute through the sandbox runner, so on a deployment without one they would fail on every call.

Artifacts

An artifact is a file the assistant produces that you can open, download, and edit later. Artifacts are versioned: every edit appends a new version at the same artifact, so you can review or restore an earlier one. Bytes are stored on the server and are never passed through the model, so large files stay fast and cheap to work with.

In a normal chat, a produced artifact appears as a chip under the assistant’s reply (for example, “Artifact”). A chip opens the artifact’s current version, so a chip under an earlier reply shows the latest edit. Open it to:

  • preview the content in a side panel (HTML and similar kinds render inline),
  • download the file,
  • ask for changes, which create a new version.

Downloads need the API served through the ASGI app (docsgpt.asgi:asgi_app), as docsgpt api, docsgpt dev and the Docker images do. Under flask run, GET /api/artifacts/<id>/download returns 404. See ASGI-only features.

Downloading artifacts through the API

Responses of the OpenAI-compatible API, /stream and /api/answer list the artifacts a turn made, each with a download_url. A client sends the agent’s API key in an Authorization: Bearer header to fetch it, so the key never goes into the URL. An agent key can read the artifacts of its own agent’s conversations only, needs the conversation’s id, which the URL carries, and can’t delete or restore anything. Set PUBLIC_API_BASE_URL to make the URLs absolute.

The Artifact tool

The Artifact tool keeps a structured spec as the source of truth and renders the file from it. Editing means changing the spec and re-rendering, which keeps a clean and diffable version history. It exposes three actions:

  • create a new artifact from a kind and a spec,
  • edit an existing artifact with a targeted change (preferred for small edits),
  • rewrite an artifact with a full replacement spec.

Supported kinds are presentation (.pptx), document (.docx), spreadsheet (.xlsx), pdf, and html.

Word documents

The document kind takes a list of blocks, so contracts, memos and reports keep their structure:

BlockFields
headingtext or runs, level 1-6, align
paragraphtext or runs, style (any Word paragraph style in the document, such as Body Text), align (left, center, right, justify)
listitems, ordered, start. An item is a string or {"text" or "runs", "items": [...]}; nested numbered items are numbered 1., 1.1., 1.1.1. Each list starts its own count; start sets the first number.
tablerows, headers, widths (one relative number per column, such as [3, 1]), style (a Word table style, Table Grid by default). A cell is a string, a number, or {"text" or "runs", "align"}. The header row is bold, shaded and repeated on every page.
quotetext or runs
codetext, in a monospace font
page_breaknone

text is always literal. For bold, italic and other formatting, use runs: a list of strings and {"text", "bold", "italic", "underline", "strike", "code", "link"} objects. link takes an https://, http:// or mailto: address. Formatting is never parsed out of the text, so an asterisk or an underscore in a clause stays as typed.

{ "title": "Services Agreement", "blocks": [ {"type": "heading", "text": "Fees", "level": 1}, {"type": "paragraph", "runs": ["The ", {"text": "Client", "bold": true}, " pays within 30 days."]}, {"type": "list", "ordered": true, "items": ["Setup", {"text": "Support", "items": ["Hours", "Response times"]}]}, {"type": "table", "headers": ["Service", "Fee (EUR)"], "rows": [["Setup", 1000], ["Support", 200]], "widths": [3, 1]} ] }

The earlier shape, {"sections": [{"heading", "paragraphs": [...]}]}, still works and renders as before, and edit_artifact can append blocks to a document made that way. Footnotes aren’t supported.

Non-Latin text in PDF artifacts

The pdf kind draws text with the fonts installed in the sandbox, so text in other scripts renders instead of showing as boxes or blanks:

  • Latin, Greek, Cyrillic and most symbols (such as −, → and ≤) use DejaVu Sans.
  • Arabic, Persian, Hebrew, Devanagari and the other Indic scripts, Thai, Khmer, Ethiopic and a few more use the matching Noto font. A paragraph can mix scripts: each run of text gets a font that has its characters.
  • Chinese, Japanese and Korean use the standard PDF CJK fonts (STSong-Light, HeiseiKakuGo-W5 and HYGothic-Medium). They aren’t embedded, so the PDF viewer supplies the glyphs, and they have no bold weight.
  • Right-to-left paragraphs are joined, put in display order and right-aligned. Devanagari, Bengali, Tamil, Thai and similar scripts are shaped with HarfBuzz, so conjuncts and vowel signs sit where they belong; text copied out of a shaped word can contain private-use characters.

A sandbox image without these fonts falls back to matplotlib’s copy of DejaVu Sans, then to Helvetica, which covers Latin only. For a layout the pdf kind can’t express, the model can build the file in the Code Executor and convert HTML with html-to-pdf.

Code Executor

The Code Executor runs Python in a sandboxed session bound to your conversation (or to a workflow run). Files the code writes into the workspace are captured as artifacts automatically, so a script that produces report.csv gives you a downloadable artifact with no extra steps.

Key points:

  • The session stays open between runs until it has been idle for SANDBOX_MAX_TTL seconds (1200 by default) or a shorter time the model asks for. The model can pass persist=false to close it after a run.
  • What carries over to the next run depends on the backend. On the Jupyter runner, variables, imports, files, and installed packages all do. On Daytona, files and installed packages do, but each run starts a new Python interpreter, so the model re-imports and re-loads what it needs and passes data between runs through files.
  • Each result tells the model whether the session is new (nothing from earlier runs is left, for example after it idled out) or reused, so it knows when to rebuild its files.
  • Only a compact summary is returned to the model (an output tail plus artifact references), never raw file bytes.
  • Each run is limited to SANDBOX_EXEC_TIMEOUT seconds of wall-clock time (60 by default). For a long job, such as a video render, OCR of many pages or converting a large deck, the model can pass a timeout of up to SANDBOX_EXEC_MAX_TIMEOUT seconds (1000 by default); a larger value is cut to the maximum and the result says so. In a chat, a run that is still going after BACKGROUND_YIELD_SECONDS (30 by default) becomes a background job: the turn ends, the run keeps going in the sandbox, and the agent comes back with the result when it finishes (in conversations through the API, a widget or a shared link the result waits for check_job or the next message instead). The model can also pass background=true to start a run as a job right away; without a timeout, such a run may take up to BACKGROUND_JOB_MAX_SECONDS (still within SANDBOX_EXEC_MAX_TIMEOUT).
  • When a run uses more memory than the sandbox allows, the result says it ran out of memory rather than timed out, with a hint to load and process less at once.
  • Every file a run creates or changes in the workspace is saved as an artifact, except files under scratch/ (or tmp/), where the model puts previews and intermediate files. The model can instead name the files or globs to keep. Files written outside the workspace, such as under /tmp, are never saved.
  • Saving a file under a name the Code Executor already used in the conversation (or workflow run) adds a new version of that artifact instead of a second artifact, and saving identical bytes again adds nothing. Files from workflow Code nodes are always new artifacts.
  • Install other packages from inside the code itself when you need them.

By default the Code Executor runs model-authored code without an approval prompt. Turn on per-action approval on the agent if you want a human to confirm each run.

What the sandbox has installed

The self-hosted runner image and the Daytona snapshot hold the same set, listed in docsgpt/sandbox/manifest.py, and the Code Executor tells the model what is there so it doesn’t have to find out by failing:

  • Python libraries: pandas, numpy, matplotlib, openpyxl, python-docx, python-pptx, reportlab (with uharfbuzz for shaping, and arabic-reshaper and python-bidi for right-to-left text), lxml, Pillow, requests, beautifulsoup4, PyYAML, pypdf, PyPDF2, pdfplumber, pdfminer.six, pypdfium2, pytesseract and imageio.
  • Commands: headless LibreOffice, headless Chromium, tesseract OCR (English), poppler’s pdftotext and pdftoppm, ffmpeg and ffprobe, and Node.js 24 with npm and npx.
  • Conversion helpers: office-convert FILE --to pdf (also docx, xlsx, pptx, png and more) converts office documents with LibreOffice; html-to-pdf and html-screenshot render an HTML file or URL with Chromium.
  • Fonts: DejaVu, Liberation and Carlito/Caladea (metric-compatible with Arial, Times New Roman, Courier New, Calibri and Cambria), Noto for Arabic, Devanagari and other scripts, and Noto CJK for Chinese, Japanese and Korean.

imageio writes animated GIF and WebP; for MP4 and other video the code runs ffmpeg. PyMuPDF (fitz) is not installed because of its AGPL license; pdfplumber, pypdf and pypdfium2 cover PDF reading.

What the model is told

The Code Executor’s tool description is under 300 words and depends only on the deployment (backend, SANDBOX_EXEC_TIMEOUT, SANDBOX_EXEC_MAX_TIMEOUT, SANDBOX_MAX_TTL and the manifest), never on the conversation, so providers can cache it. It tells the model:

  • what carries over between calls on this backend, and that a result marked session: new means it starts again;
  • that every file it writes in the working directory becomes a download, except under scratch/, that files elsewhere (such as /tmp) are never saved, and to save each deliverable once under its final name;
  • to show a chart with plt.show() to check it and savefig() the one the user should get, and to name saved files in its answer rather than link them;
  • to pass earlier files by reference (A1, F3) in inputs rather than download artifact URLs, which need the user’s session;
  • the default time cap, that it can pass a timeout up to the maximum for long jobs, and to prefer splitting work. The timeout parameter gives rough budgets: about 120 seconds for installing a big package or converting a large deck, 2-3 seconds per page of OCR, and a video render in proportion to its frames;
  • the preinstalled libraries by import name, the commands, and which library makes which format: reportlab for PDF (html-to-pdf for non-Latin text), python-docx, openpyxl and python-pptx for Office files, office-convert for conversions and ffmpeg for video;
  • to use the Artifact tool for documents the user will keep editing;
  • not to pip-install a listed library or run apt-get.

The first result of a new session also carries an environment field, under 120 words: the packages with their import names, how to call office-convert, html-to-pdf and html-screenshot, the other commands, each font’s file and the scripts it covers, the workspace paths, and the limits. A reused session’s results leave it out. A new session also starts with an empty scratch/ directory, so commands that don’t create directories (such as pdftoppm) can write there.

Hints in results

When a run shows a mistake the Code Executor recognizes, the result gets a hint list with a line or two for each. Package, command and font names in the hints come from the manifest.

The run showsThe hint says
An import of a package that isn’t installedWhich preinstalled library or command to use instead (for example pdfplumber for fitz), or to install the package in the same call
pip install of a preinstalled packageIt is already installed; drop the pip step
apt-get, apt or sudo in the codeSystem packages can’t be installed; the commands that exist
A missing file or variable in a new sessionEarlier files and variables are gone; recreate them or pass them as inputs
A missing variable on DaytonaEach call is a new interpreter; re-import and reload from files
A missing file under /tmp/tmp isn’t kept; use scratch/ or the workspace
The time capWork not written before the cap is lost; split it and write partial results to scratch/. Below the maximum, also to rerun with a larger timeout, except for a background run, which is reported and re-run only if the user asks
Running out of memory: the kernel or process killed for memory, a MemoryError, or a subprocess killed with SIGKILLLoad less at once: read in chunks, one page or frame at a time, at a lower resolution
A missing command (wkhtmltopdf, pdftk, google-chrome, …)The replacement (html-to-pdf, pypdf, …) and the commands that exist
A 401, 403 or connection error fetching this deployment’s URLs or /api/artifacts/Pass the file in inputs instead
A PDF, Office file, image, CSV, HTML, MP4 or ZIP written to an absolute path outside the workspaceIt was not saved; save it in the workspace
More than three images saved, or files named like preview, test, tmp, debug or checkWhich files the user got; put previews in scratch/
A font file that can’t be opened, or missing glyphsThe installed fonts with their files and scripts
reportlab with non-Latin text in its built-in fonts, or with the Noto CJK collection (which it can’t load)Use html-to-pdf, which embeds the fonts and shapes Arabic and Devanagari, or a font reportlab can load
A package installed at runtime whose compiled code won’t load (only on a runner started without its /sandbox-home mount, where the home falls back to the noexec /tmp)Use a preinstalled or pure-Python library
A successful run with no output and no filesPrint what it needs to see
The same error twice in a row in one conversationChange approach instead of retrying

Before the output is cut to its last 4,000 characters, colour codes and pip’s routine lines (“Requirement already satisfied”, the root-user warning, upgrade notices, download progress) are removed. The runner image also stops ipykernel from forcing colour on the programs code starts, so Node and npm print plain text. On Daytona, a failed run’s error names the exception from the last traceback line instead of repeating the whole output.

Run inputs

You can pass files into a run. Each input accepts a short reference returned by a previous artifact action, a full artifact id, or the name or id of a file you attached to the conversation. Each referenced file is placed in the workspace at inputs/<filename> before the code runs.

Letting the model see images

When the model reads images, it can look at what its tools produce instead of guessing:

  • Charts a run displays with plt.show() are shown to the model with the run’s result. They are saved as artifacts too, unless the run also saved an image file of its own (with savefig), so a chart the code both saves and shows is saved once. On Daytona this needs a snapshot that includes matplotlib (see Daytona snapshot).
  • A view_image tool is added next to the Code Executor and Read Webpage. It shows the model an image artifact by its reference (such as A2) or an image URL, such as one on a page Read Webpage returned.
  • Images an MCP server returns are shown to the model as images too.

The image goes inside the tool result for Anthropic, the OpenAI Responses API, and Gemini 3. For Chat Completions and earlier Gemini models it follows the tool results in a separate message. Some OpenAI-compatible providers handle this differently: their Responses API can drop images inside a tool result, while some Chat Completions servers (OpenRouter, for example) accept them there. Set tool_result_images on the model (follow_up or native) in its model YAML, or in Settings for a custom model, to choose for that model. Images are scaled down to at most 2000 pixels on the long side, and an earlier turn’s images are not sent again: the model views them again when it needs to.

Read Document

Read Document parses a file into text, markdown, structured JSON (with tables), or chunks. It is meant for workflows where a node needs the contents of an uploaded or produced document, for example a compliance flow that extracts fields and validates them against a JSON schema.

Parsing runs in the DocsGPT parsing worker rather than the sandbox, so it uses the same document engine as the rest of the product and works with every sandbox backend.

Using these tools in workflows

Inside an agent workflow the same building blocks are available:

  • A Code node runs a script bound to the run, with the workflow state available to it as data.
  • Agent and code nodes can receive selected documents as inputs and hand files to each other by reference.
  • Produced artifacts are visible from the workflow run view.

See Workflow Nodes for the node-level details.

Configuration

Code execution and rendering run inside a sandbox. DocsGPT ships two backends, selected with SANDBOX_BACKEND:

  • jupyter (default): a self-hosted runner that you operate, added with a Compose overlay, a Kubernetes manifest, or the arc53/docsgpt-sandbox image. It needs a shared SANDBOX_GATEWAY_AUTH_TOKEN. One runner is one trust domain: sessions are separated only by working directory.
  • daytona: Daytona Cloud, a managed per-session sandbox. Set DAYTONA_API_KEY and choose a region with DAYTONA_TARGET. Use it for multi-tenant or untrusted workloads.

Code Execution Sandbox has the setup steps for each.

Daytona snapshot

Daytona’s default image is plain Python: presentation, document, spreadsheet and PDF rendering fail there, and none of the commands above exist. HTML artifacts need no extra libraries and work on any image. Build a snapshot with the sandbox’s contents once, then set DAYTONA_SNAPSHOT to it:

# Reads DAYTONA_API_KEY / DAYTONA_API_URL / DAYTONA_TARGET from your environment: python scripts/build_daytona_snapshot.py # then set in your environment: # DAYTONA_SNAPSHOT=docsgpt-sandbox-py312-v3

Sandboxes made from the snapshot get 2 vCPUs, 2 GiB of memory and 6 GiB of disk, double Daytona’s default, because LibreOffice and Chromium need the room. Change them with --cpu, --memory and --disk. --smoke runs the image’s smoke test in a sandbox from the new snapshot, and --dockerfile prints the image definition without calling Daytona.

A snapshot can’t change once it’s built, so a new version of the contents comes with a new name. Snapshots built by earlier versions of the script (docsgpt-artifacts-py312, docsgpt-sandbox-py312) lack most of the tools above: build the new one and switch DAYTONA_SNAPSHOT to it.

Useful settings

  • SANDBOX_BACKEND: jupyter or daytona.
  • SANDBOX_GATEWAY_URL, SANDBOX_GATEWAY_AUTH_TOKEN, SANDBOX_KERNEL_NAME: where the Jupyter runner is, its shared token, and the kernel each session uses (docsgpt-python, which strips secrets from the kernel environment).
  • SANDBOX_EXEC_TIMEOUT: per-run wall-clock cap in seconds when the model doesn’t pass a timeout.
  • SANDBOX_EXEC_MAX_TIMEOUT: the largest timeout the model may pass.
  • SANDBOX_MAX_TTL: how long an idle session is kept before it is closed.
  • SANDBOX_MAX_FILE_BYTES (10 MiB): the largest workspace file saved as an artifact. A bigger one is not saved, and the run’s result names it (not_saved: “race.mp4 (38.9 MB) is over the 10 MB limit and was not saved; compress, downscale or split it”), so the model can say so or shrink it. Capture reads each file whole into memory (on the Jupyter runner as base64 over the kernel’s output), so the default stays at 10 MiB; raise it only if the API and worker have the memory, and never above ARTIFACT_MAX_BYTES (50 MiB), which caps any stored artifact.
  • DAYTONA_API_KEY, DAYTONA_TARGET, DAYTONA_SNAPSHOT: Daytona Cloud settings.
  • Per-user artifact quotas bound how many artifacts and how much storage each user can hold.
Last updated on