Skip to main content

Reproduce a failed render

A render failed in production, and the error points at line 41 of the template. To fix it you need what caused it: the data of that request. When the workspace keeps render requests, one command brings that data into your project as a data set, renders it locally and shows the error with its line and column.

formfeed renders pull rnd_7da09ab6cd9946b798c187c2a9350852 --run

1. Keep render requests​

Formfeed does not keep the data of a request unless the workspace asks for it. An owner or admin turns it on under Settings, in the section of the workspace (Workspace “…”), with Keep render requests. Each workspace has its own setting.

SettingPlansWhat is kept
Off (default)AllNothing. An async render holds its request only while it waits in the queue; it is deleted the moment the render completes
24 hoursStarter and aboveThe request of every render, sync and async, for 24 hours after the render completed
7 daysPro and aboveThe same, for 7 days
  • The setting applies from the moment it is turned on. Renders that completed before cannot be pulled.
  • The plan caps the choice: after a downgrade, requests are kept only as long as the new plan allows, starting at once (a workspace set to 7 days keeps 24 hours on Starter and nothing on Free).
  • Passwords of post-processing are never kept. A password-protected render therefore cannot be re-rendered from the render log, because the copy would come out unprotected.
  • Renders of test keys and live keys are kept alike.
  • A request is deleted when its period ends (the cleanup runs every hour), and in any case 7 days after it arrived.

A kept request can contain personal data: names, addresses, IBANs. Turn the setting on when you need it, prefer 24 hours, and read Personal data below.

2. Find the render​

The render id (rnd_…) is in the render log of the app, in the render.failed webhook, in the response of the request, and in the CLI:

formfeed renders list --status failed --since 2026-09-13T00:00:00Z

3. Pull it into the project​

Run this inside a project made with formfeed init and formfeed templates pull:

formfeed renders pull rnd_7da09ab6cd9946b798c187c2a9350852

The CLI reads the stored request and writes the data to templates/<slug>/data/render-<last 6 characters of the id>.json, next to the template's other data sets. The template needs a folder in the project (formfeed templates pull <slug> first), and only renders of a template can be pulled, not raw HTML or URL renders.

  • --as <name> picks the file name: --as missing-vat-id writes data/missing-vat-id.json. Letters, digits, _, . and -; default is reserved.
  • An existing file is not overwritten unless you pass --force.
  • When the render used a different template version than the one in your folder, the CLI names both version numbers. The data may not fit your local template then; formfeed templates pull brings the folder up to date.

Pulling needs the render:input scope. formfeed login gives it to editors, admins and owners; an API key has it only when you tick it when you create the key.

When the request cannot be pulled, the command stops with a message and exit code 2 (the request is not kept, or the render is not in this workspace) or 3 (the key lacks render:input); see exit codes.

4. Run it locally​

formfeed renders pull rnd_7da09ab6cd9946b798c187c2a9350852 --run

--run renders the new data set with the local engine right away, as formfeed test does, with the locale of the original request, and prints the diagnostic with the template file, line and column. When the template does not render, the command exits with code 1, so it also works in scripts. For a failed render, that is the reproduction in one command. Afterwards, the data set works like any other:

formfeed dev invoice # live preview while you fix the template
formfeed validate # compile every template against its data sets

Fix the template, push a new version, and keep the data set: from now on, formfeed validate in CI catches the same mistake before it reaches production.

5. Turn a good render into a test​

A render that came out right is a regression test waiting to happen:

formfeed renders pull rnd_f3c70d646f1ee82bde1e266ee2689a9b --as order-with-40-lines --snapshot

--snapshot renders the data locally and writes the result to templates/<slug>/tests/__snapshots__/order-with-40-lines.html (.xml for a Word or PowerPoint template). formfeed test then fails whenever a template change alters that document. For a failed render, which has nothing worth approving, the CLI refuses --snapshot (exit code 2); pull it with --run instead.

Personal data​

A pulled data set is a copy of a customer's request on your disk. Before you commit it, redact it:

formfeed renders pull rnd_7da09ab6cd9946b798c187c2a9350852 --redact

--redact replaces values but keeps the shape of the data, so the template still takes the same branches:

ValueBecomes
StringsPlaceholders of the same length that keep the character classes (uppercase letters become A, other letters a, digits 0, everything else stays): jane@mail.test becomes aaaa@aaaa.aaaa, DE36 0000 0000 0000 0000 00 becomes AA00 0000 0000 0000 0000 00
NumbersKept, because they are usually what the bug is about
Booleans and nullKept

With paths, only those fields are redacted: --redact customer.* items[].name iban. A path is dotted from the root of the data; * stands for any one key or array item, [] for every item of an array, and a path that ends on an object or array redacts everything below it. Quote paths with * in a shell that expands them. To make that the default for everyone in the project, set it in formfeed.json:

{
"pull": { "redact": ["customer.*", "iban"] }
}

With that setting, every formfeed renders pull redacts those paths, and --redact without paths does too; --no-redact skips it for one pull. Without any redaction the CLI reminds you that the file may contain personal data. Some more advice:

  • Keep the retention as short as your debugging needs. Turn it off again once the problem is solved.
  • Give render:input only to people and keys that debug templates. A production integration never needs it.
  • Every read of a stored request, from the CLI, the API or the app, is written to the organisation's audit log as render.input_read. The Activity page shows the audit log on Pro (last 30 days), Business (365 days) and Enterprise.
  • Delete pulled files you no longer need, and do not share unredacted ones in tickets or chats.

In the app​

Open Renders and click a render to open its detail sheet. Its Request section offers three actions to editors, admins and owners while the request is kept (when it is not, the section says so and links to the settings):

  • Open in editor with this data (template renders only) opens the template in the editor with the request's data as a new, unsaved data set, named like the CLI's, and the version the render used selected when it is not the latest. Nothing is saved until you save; then the data set becomes part of a new version like any data set change.
  • Copy as curl copies the request against POST /v1/renders with the data inline and $FORMFEED_API_KEY as a placeholder, never a real key.
  • Download request (JSON) saves the stored request, in the shape of the API's answer.

Re-render at the foot of the sheet (editors, admins and owners) queues the same request again in the workspace: the same template version, data, options and environment, as an async render. It appears in the render log as a new render whose meta carries replay_of with the original's id, and costs units like any render. It needs the kept request, and it is refused for a password-protected render and for a PDF tool call, which you repeat through the API.

The failed document. When a render fails after its HTML was assembled (a template or html render, not a url render), Formfeed keeps that assembled HTML for 24 hours as the output failed-render.html, under Outputs in the sheet, for every member, whether requests are kept or not. It contains the request's data, which is why it is private and deleted after a day. Open it in a browser to see what Chromium was given.

Through the API​

GET /renders/{id}/input returns the stored request, with a key that has render:input:

curl https://api-eu.formfeed.dev/v1/renders/rnd_7da09ab6cd9946b798c187c2a9350852/input \
-H "Authorization: Bearer $FORMFEED_API_KEY"
{
"render_id": "rnd_7da09ab6cd9946b798c187c2a9350852",
"template": { "id": "tpl_7c1e…", "slug": "invoice", "version": 12, "channel": "published" },
"data": { "invoice": { "number": "2026-0042", "lines": [ … ] } },
"locale": "de-DE",
"output": "pdf",
"meta": { "order_id": "8812" },
"environment": "live",
"created_at": "2026-09-13T08:14:02Z"
}

It is what renders the document again, not an internal record: the template and version, the data, the locale, the output and the options (settings, filename, post without passwords). A render from raw HTML carries html and engine instead of template, a URL render url. Fields that do not apply are left out.

When the workspace does not keep requests, or the period has ended, the answer is 410 render_input_expired with the workspace's current request_data_retention (off, 24h or 7d) in the problem:

{
"type": "https://docs.formfeed.dev/errors/render-input-expired",
"status": 410,
"code": "render_input_expired",
"detail": "The request of this render was not kept: the workspace does not keep render requests (Settings, Keep render requests)",
"request_data_retention": "off"
}

A render id that is not in the key's workspace answers 404 not_found, a key without render:input 403 forbidden. Every successful read is audited like the CLI's. The SDKs have client.renders.input(id) in TypeScript and Python.