Render Forms
A generic form system that auto-generates user-facing forms from any template's form field definitions. Works with all template types (pintora, echart, typst, html, image_compose, etc.).
Overview
Templates define user-fillable fields via the dedicated form_fields JSONB
column (preferred) or the legacy seed_json._form array. The system:
- Extracts form field definitions from the template
- Renders a dynamic HTML form with validation
- Submits user input to the render engine
- Displays the rendered output inline
API
Get form metadata
GET /v1/render/form/{idOrKey}?company_id=uuid
Resolves the template by ID, template_key, or @shortref code. Returns:
{
"template_id": "template_01jq5...",
"template_key": "my_template",
"short_ref": "@k7f2n",
"description": "My Template",
"source_type": "pintora",
"form_fields": [...],
"defaults": {...},
"output_formats": ["svg", "png", "pdf"]
}
Render with form data
Use the standard render endpoint with the user's form values:
POST /v1/render
{
"company_id": "uuid",
"template_id": "template_01jq5...",
"output_format": "svg",
"data": { "field1": "user_value", "field2": 42 },
"inline": true
}
form_fields Column (Recommended)
The form_fields JSONB column on the templates table is the preferred way to
define form fields. It's a top-level array of field objects:
[
{
"key": "team_name",
"label": "Team Name",
"type": "text",
"required": true,
"placeholder": "Engineering"
},
{
"key": "style",
"label": "Style",
"type": "select",
"options": ["default", "dark", "forest"],
"default": "default"
}
]
Default values for these fields live in seed_json as sibling keys (e.g.,
"team_name": "Engineering").
Legacy: _form Convention
The _form array inside seed_json is still supported as a fallback. The API
prefers form_fields when non-empty, otherwise reads seed_json._form. The
_form key is metadata only — stripped before merging into render data.
{
"_form": [
{
"key": "team_name",
"label": "Team Name",
"type": "text",
"required": true,
"placeholder": "Engineering",
"description": "Name of the team"
},
{
"key": "style",
"label": "Style",
"type": "select",
"options": ["default", "dark", "forest"],
"default": "default"
},
{
"key": "max_depth",
"label": "Max Depth",
"type": "number",
"min": 1,
"max": 10,
"default": 3
}
],
"team_name": "Engineering",
"style": "default",
"max_depth": 3
}
Field Types
| Type | HTML Element | Output | Notes |
|---|---|---|---|
text |
<input type="text"> |
string | maxLength, pattern |
textarea |
<textarea> |
string | rows=4 default |
number |
<input type="number"> |
number | min, max, step |
select |
<select> |
string | options array |
checkbox |
<input type="checkbox"> |
boolean | |
date |
<input type="date"> |
ISO string | |
color |
<input type="color"> |
hex string | |
email |
<input type="email"> |
string | validation |
url |
<input type="url"> |
string | validation |
hidden |
(not rendered) | any | passed silently |
Field Properties
| Property | Type | Required | Description |
|---|---|---|---|
key |
string | yes | Variable name used in templates |
label |
string | yes | Display label |
type |
string | yes | Input type (see above) |
required |
boolean | no | Validation flag |
default |
any | no | Pre-fill value |
placeholder |
string | no | Input placeholder |
description |
string | no | Help text below field |
options |
string[] | no | Options for select type |
min |
number | no | Min for number type |
max |
number | no | Max for number type |
Frontend Route
/render/:key — resolves by template_key, @shortref, or template ID.
The page:
- Fetches form metadata from the API
- Renders a dynamic form from
_formdefinitions - Pre-fills defaults from
seed_json - On submit: renders via POST /v1/render
- Displays result inline (SVG/image) or offers download
Integration with $fn:
$fn: data functions in seed_json are resolved before form defaults are
applied. This means forms can show live database values as defaults:
{
"_form": [
{
"key": "period",
"label": "Period",
"type": "select",
"options": ["7d", "30d", "90d"]
}
],
"total_customers": "$fn:count(customers)",
"period": "30d"
}
The total_customers value is resolved to a real number before Nunjucks
rendering, even though it's not a form field.
CLI
# Inspect form fields
docuget render my_template --form-info
# Interactive mode — prompts for each _form field
docuget render my_template --interactive --out=svg
# Override fields directly
docuget render my_template --out=pdf --field company_name="Acme" --field date=2026-03-25
# Generate signed shareable URLs
docuget share my_template --ttl=86400
# Output:
# View: https://front1.docuget.cloud/render/my_template?company_id=...&sig=...&exp=...
# Form: https://front1.docuget.cloud/render/my_template/form?company_id=...&sig=...&exp=...
Sharing
Signed URLs allow anyone to access the render view or fillable form without logging in:
- View URL (
/render/:key?sig=...) — renders immediately with seed_json defaults - Form URL (
/render/:key/form?sig=...) — shows the fillable form, user edits fields, clicks Render
Generate from:
- CLI:
docuget share <template_key> --ttl=86400 - API:
GET /v1/render/form/:key/sign?company_id=...&ttl=86400 - UI: Click "Share" button in Data Science or Templates editor
Signatures use HMAC-SHA256 with the RENDER_SIGN_SECRET env var. Default TTL is
24 hours.