Embeds — Integration Guide
Looking to embed rendered output (PDF/HTML) rather than an interactive surface? See embedding-guide.md.
Pulp Engine ships two companion custom elements:
<pulp-engine-editor>— the full template editor. For operators who let end users design and edit templates in-product.<pulp-engine-form>— a fillable form derived from a template’sinputSchema. For end users who only need to enter data and receive a rendered PDF.
Both elements are iframe-isolated, share the same auth and CSP configuration, and are distributed by the Pulp Engine API server. Most of this guide covers the editor; the Form Embed section at the bottom documents the form element.
Install and compatibility
npm install @pulp-engine/editor-embed @pulp-engine/form-embed
# React 18 or later:
npm install @pulp-engine/embed-react
These ESM packages are typed wrappers. They load the custom element and iframe from your own Pulp Engine server; the designer itself stays on that server. Keep the wrapper minor version at or below the server minor within the same major. New methods feature-detect the server element and report unsupported features clearly. The plugin API has a separate 0.2.x version line.
Quick Start
1. Load the embed script
Add the script tag pointing at your Pulp Engine API server:
<script src="https://your-pulp-engine-server.com/pulp-engine-embed.js"></script>
2. Add the editor element
<pulp-engine-editor
api-url="https://your-pulp-engine-server.com"
template-key="invoice-v2"
token="et_..."
allowed-nodes="heading,text,table,image,container,columns"
theme="light"
style="width: 100%; height: 700px;"
></pulp-engine-editor>
3. Listen for events
const editor = document.querySelector('pulp-engine-editor')
editor.addEventListener('saved', (e) => {
console.log(`Template ${e.detail.key} saved as version ${e.detail.version}`)
})
editor.addEventListener('dirty-change', (e) => {
// Show/hide a "You have unsaved changes" banner
setBannerVisible(e.detail.isDirty)
})
Authentication
Recommended: Server-side token minting
Your backend mints short-lived editor tokens by calling the Pulp Engine API. The browser never sees the API key.
[Your Backend] --POST /auth/editor-token--> [Pulp Engine API]
|
{ token, expiresAt }
|
[Your Frontend] <-- pass token via props -----+
Pass the token to the editor element:
<pulp-engine-editor
api-url="https://pulp-engine.internal"
template-key="invoice-v2"
token="et_abc123..."
token-expires-at="2026-04-10T12:00:00Z"
></pulp-engine-editor>
When the token nears expiry, mint a new one server-side and refresh:
const editor = document.querySelector('pulp-engine-editor')
editor.setToken(newToken, newExpiresAt)
Development only: API key attribute
For prototyping, you can pass the API key directly. Do not use in production — the key is visible in the DOM.
<pulp-engine-editor
api-url="http://localhost:3000"
api-key="your-editor-key"
template-key="invoice-v2"
></pulp-engine-editor>
Server Configuration
EMBED_ALLOWED_ORIGINS (required for production)
By default, the Pulp Engine server blocks iframe embedding via CSP frame-ancestors 'none'. To allow embedding, set:
EMBED_ALLOWED_ORIGINS=https://app.acme.com https://staging.acme.com
Space-separated list of origins permitted to embed the editor. This sets frame-ancestors on /embed.html responses.
EMBED_CONNECT_ORIGINS (optional)
Space-separated list of extra origins the embedded editor is permitted to open fetch/WebSocket connections to (connect-src in the embed CSP). Only set this if the embedding host needs to proxy API calls through a domain distinct from the Pulp Engine server.
EMBED_CONNECT_ORIGINS=https://forms-proxy.acme.com
CORS
If your frontend is on a different origin than the Pulp Engine API, ensure CORS_ALLOWED_ORIGINS includes your frontend origin:
CORS_ALLOWED_ORIGINS=https://app.acme.com
Attributes
| Attribute | Required | Description |
|---|---|---|
api-url |
Yes | Base URL of the Pulp Engine API server |
template-key |
No | Template key to auto-load on init |
token |
No* | Pre-minted editor token (recommended for production) |
token-expires-at |
No | ISO date string for token expiry |
api-key |
No* | Raw API key (dev/prototyping only) |
allowed-nodes |
No | Comma-separated list of allowed node types |
theme |
No | light (default) or dark |
locale |
No | UI language: en, de, fr or ar; regional tags match their base language. Updates live. |
locale-selectable |
No | true lets users change a host-selected editor language. Otherwise the Language menu is hidden when locale is supplied. |
preview |
No | true (default) or false to hide preview |
brand |
No | JSON brand tokens; see white-labelling below |
hidden-features |
No | Comma-separated feature keys, updated live |
readonly |
No | Boolean attribute; use readonly="false" or remove it to enable editing |
locked-properties |
No | Comma-separated property paths, updated live |
api-auth |
No | editor-token (default) or sandbox-token |
* Either token or api-key must be provided for authenticated servers.
Embed-mode defaults
The element and SDK default to one template, without tabs, a template picker or a New Template button. Preview stays enabled. Help and onboarding default off; collaboration follows the server capability. Raw pulp-engine:init integrations may enable the existing features flags. None of the new white-label options change these defaults.
setTemplate(key) replaces the active template and refreshes its preview data. Hosts may also supply an inline definition and preview data through the methods below.
Events
All events are standard DOM CustomEvents dispatched on the <pulp-engine-editor> element. Use addEventListener() to subscribe.
| Event | detail fields |
Description |
|---|---|---|
ready |
protocolVersion: 2 |
Editor loaded and authenticated |
template-loaded |
key, version |
Template fetched and rendered |
dirty-change |
isDirty |
Unsaved changes state changed |
saved |
key, version |
Template saved successfully |
save-error |
message |
Save failed |
action |
id, templateKey, version, isDirty |
Host toolbar action selected |
snapshot |
requestId, template, previewData |
Snapshot response |
snapshot-error |
requestId, message |
Snapshot unavailable |
error |
message, code |
Auth/API/runtime error |
Error codes
| Code | Meaning |
|---|---|
AUTH_FAILED |
Token exchange or API key validation failed |
AUTH_EXPIRED |
Editor session expired — provide a new token |
TEMPLATE_LOAD_FAILED |
Could not fetch the specified template |
READ_ONLY |
Host requested a save while view-only mode is active |
Methods
| Method | Description |
|---|---|
save() |
Programmatically trigger a save |
setToken(token, expiresAt) |
Refresh the auth token |
setTemplate(key) |
Load a different template |
setTheme(theme) |
Switch light/dark live |
setLocale(locale, selectable?) |
SDK: change UI language live; null removes the host override. Optional selectable controls the Language menu. |
setBrand(brand) |
Replace brand tokens live |
setConfig(partial) |
Update brand, hiddenFeatures, readOnly, lockedProperties or toolbarActions |
setToolbarActions(actions) |
Replace up to five host actions |
setInlineTemplate(definition) |
Load a definition without fetching it |
setPreviewData(data) |
Replace preview data |
requestSnapshot() |
Promise of {template, previewData}; rejects if unavailable |
getSnapshot() |
SDK alias for requestSnapshot() |
White-labelling and UI configuration
const editor = createEditor({
apiUrl: 'https://pulp.example.com', token, tokenExpiresAt,
brand: { productName: 'Acme Reports', primary: '#0f766e', radius: '4px',
logoUrl: '/acme-logo.svg', fontFamily: 'Inter, sans-serif',
dark: { primary: '#5eead4', primaryForeground: '#000000' } },
hiddenFeatures: ['ai', 'schedules'], readOnly: true,
lockedProperties: ['renderConfig.margin', 'style.fontFamily'],
toolbarActions: [{ id: 'approve', label: 'Approve', icon: 'check' }],
})
editor.on('action', ({ id, templateKey, version, isDirty }) => {
if (id === 'approve') approve(templateKey, version, isDirty)
})
editor.mount(document.querySelector('#designer')!)
| Brand field | CSS token / behavior |
|---|---|
primary |
--primary, --ring; #RRGGBB converted to HSL triplets |
primaryForeground |
--primary-foreground; below 4.5:1 contrast selects black/white and warns |
accent |
--accent |
radius |
--radius; 0-24px or 0-1.5rem |
fontFamily |
--editor-ui-font; letters, digits, spaces, underscores, quotes, commas and hyphens; at most 200 characters |
productName |
Logo alternative text, screen-reader text and iframe title; at most 80 characters |
logoUrl |
Replaces the header and welcome logo |
dark |
Optional primary, primaryForeground and accent overrides in dark mode |
Tokens are injected into an unlayered :root rule, with :root.dark overrides. Invalid values warn and are ignored. Logos accept same-origin paths beginning with /, or base64 PNG/JPEG/WebP/SVG data URIs up to 64 KiB. Remote logos and remote fonts are blocked by the embed CSP. Serve custom fonts from the Pulp origin and provide their font-face stylesheet there.
| Hidden feature keys | Removed UI |
|---|---|
ai |
AI generation and rewrite entry points/dialogs |
schedules, labels, history |
Corresponding header, command-menu and dialog entries |
export, duplicate, publish |
Export JSON, duplicate/save-as, or Publish controls and shortcuts |
document-tab, schema-tab, mappings-tab |
Corresponding inspector tabs; active hidden tab falls back to Properties |
custom-css, font-imports |
Document setting groups, including inherited-layout settings |
expression-playground, theme-selector, command-menu |
Corresponding controls/dialogs; Ctrl+K is suppressed when the command menu is hidden |
Unknown feature keys and property paths warn and are ignored. Attribute changes and setConfig() apply live. publish only hides the host-independent save controls; a host may still call save() when editing is enabled.
View-only mode blocks tracked, silent, bulk and history mutations, local-key renaming and saving. save() emits error with code: 'READ_ONLY'. It removes Blocks, disables the inspector, drag-and-drop and rich-text editing, and replaces Undo/Redo/Publish with a View only chip. Selection, Outline, preview and host-driven template loading remain available.
Locks accept renderConfig.<key> for any current RenderConfig field, style.<key>, inputSchema and fieldMappings. Locked controls show a lock with an administrator/brand tooltip. The store preserves locked values through single edits, silent edits, bulk styling, whole-definition edits and undo/redo. Node-specific paths such as image.src are outside v1.
At most five host actions are rendered. IDs match [a-z0-9-]{1,40}, labels are at most 32 characters, icons are send, check, download, share, mail, external-link or flag. Placement is header (default) or more-menu; variant is ghost (default) or primary. disabledWhenDirty prevents selection with unsaved changes. Actions emit an event; hosts implement the action themselves.
These are presentation-layer controls. Use API roles, template permissions and restricted credentials for server enforcement. A user with a write-capable editor token can call the API directly.
Node Constraints (allowed-nodes)
Control which block types your customers can add. Existing blocks of disallowed types remain visible and editable — they just cannot be duplicated or added.
<pulp-engine-editor
allowed-nodes="heading,text,table,image,container,columns"
...
></pulp-engine-editor>
Always-allowed types
document, section, and column are structural necessities and are always permitted regardless of constraints.
Constrainable types
All other types can be restricted: container, columns, heading, text, image, table, chart, pivotTable, richText, spacer, divider, pageBreak, conditional, repeater, formField, signatureField, positioned, toc.
Custom plugin nodes
Plugin-registered custom types use the custom:<customType> format:
allowed-nodes="heading,text,custom:barcode,custom:qrCode"
Enforcement points
Constraints are enforced at four levels:
- Palette: disallowed types hidden from the block palette
- Inline add: disallowed types hidden from canvas “+ Add block” dropdowns
- Paste: disallowed nodes stripped from pasted trees (allowed root with disallowed children → children stripped; disallowed root → paste blocked)
- Store mutations:
addNode,addNodeToBranch,duplicateNodereject disallowed types
TypeScript SDK (@pulp-engine/editor-embed)
For programmatic control with full type safety:
import { createEditor, loadPulpEngineEmbed } from '@pulp-engine/editor-embed'
// Load the custom element definition from your server
await loadPulpEngineEmbed('https://pulp-engine.internal')
// Create and mount the editor
const editor = createEditor({
apiUrl: 'https://pulp-engine.internal',
templateKey: 'invoice-v2',
token: tokenFromBackend,
tokenExpiresAt,
allowedNodes: ['heading', 'text', 'table', 'image'],
})
editor.on('saved', ({ key, version }) => {
showToast(`Saved ${key} v${version}`)
})
editor.on('dirty-change', ({ isDirty }) => {
setUnsavedBanner(isDirty)
})
editor.mount(document.getElementById('editor-container')!)
// Later: cleanup
editor.unmount()
Framework Examples
React
import { useRef } from 'react'
import { PulpEngineEditor, PulpEngineForm, type PulpEngineEditorHandle } from '@pulp-engine/embed-react'
export function TemplateEditor({ templateKey, token, expiresAt }: { templateKey: string; token: string; expiresAt: string }) {
const editor = useRef<PulpEngineEditorHandle>(null)
return <PulpEngineEditor ref={editor} apiUrl="https://pulp-engine.internal"
templateKey={templateKey} token={token} tokenExpiresAt={expiresAt}
onSaved={({ key, version }) => console.log(key, version)}
style={{ width: '100%', height: 700 }} />
}
export function InvoiceForm() {
return <PulpEngineForm apiUrl="https://pulp-engine.internal" templateKey="invoice"
onSuccess={({ filename }) => console.log(filename)} />
}
The entry has use client for Next.js. Server rendering does not touch the DOM.
Callbacks always use the latest props. Token and template-key updates preserve
the editor iframe; changes to other editor options remount it. Form option
changes remount because its element has no imperative update methods.
Vue 3
Configure Vue’s template compiler to recognize the two elements:
const compilerOptions = {
isCustomElement: tag => ['pulp-engine-editor', 'pulp-engine-form'].includes(tag),
}
// Pass compilerOptions to @vitejs/plugin-vue's template.compilerOptions,
// or app.config.compilerOptions when using the runtime compiler.
Load the server script in onMounted, then mount the vanilla createEditor or
createForm instance in a container. Call unmount() in onUnmounted. If an
async script load finishes after disposal, skip the mount.
<pulp-engine-editor api-url="https://pulp-engine.internal" template-key="invoice" />
Angular
Add CUSTOM_ELEMENTS_SCHEMA (imported from @angular/core) to the standalone
component or NgModule that uses the element. Load the server script before
showing it. For programmatic events and methods, use the vanilla wrapper in
ngAfterViewInit and call its unmount() from ngOnDestroy.
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
export const embedSchemas = [CUSTOM_ELEMENTS_SCHEMA]
<pulp-engine-editor api-url="https://pulp-engine.internal" template-key="invoice"></pulp-engine-editor>
Vanilla HTML
<script src="https://pulp-engine.internal/pulp-engine-embed.js"></script>
<pulp-engine-editor
id="my-editor"
api-url="https://pulp-engine.internal"
template-key="invoice-v2"
token="et_abc123"
allowed-nodes="heading,text,table,image"
style="width: 100%; height: 700px;"
></pulp-engine-editor>
<script>
document.getElementById('my-editor').addEventListener('saved', (e) => {
alert(`Saved version ${e.detail.version}`)
})
</script>
Form Embed (<pulp-engine-form>)
<pulp-engine-form> reads a template’s inputSchema and renders a matching web form. End users fill in the fields, click submit, and receive a rendered PDF (or DOCX / PPTX / CSV / XLSX, depending on the template’s renderer). No template-authoring surface is exposed.
The element is iframe-isolated like the editor. All schema-to-form logic lives inside /form.html served from the Pulp Engine API; the host page only needs the wrapper script.
Quick Start
<script src="https://your-pulp-engine-server.com/pulp-engine-form.js"></script>
<pulp-engine-form
api-url="https://your-pulp-engine-server.com"
template-key="invoice-v2"
api-key="your-render-key"
submit-label="Generate invoice"
style="width: 100%; height: 600px;"
></pulp-engine-form>
<script>
const form = document.querySelector('pulp-engine-form')
form.addEventListener('ready', (e) => {
console.log('Form loaded. hasFieldMappings:', e.detail.hasFieldMappings)
})
form.addEventListener('success', (e) => {
console.log(`Generated ${e.detail.filename} (${e.detail.size} bytes)`)
})
form.addEventListener('error', (e) => {
console.error(`Form error ${e.detail.code}: ${e.detail.message}`)
})
</script>
On mount, the form fetches GET /templates/:key/form-metadata, renders a React form from the returned inputSchema, and submits to POST /render/pdf when the user clicks the submit button. The resulting blob is either downloaded in-iframe (result-mode="download", default) or posted back to the host page as a Blob (result-mode="blob").
Authentication
For public end-user forms, mint a scoped render token on your backend with allowFormMetadata: true, the allowed template and PDF format, an expiry and a render budget. Pass only that token to the page:
<pulp-engine-form api-url="https://pulp.example.com" template-key="certificate"
render-token="TOKEN_FROM_YOUR_BACKEND" result-mode="blob"></pulp-engine-form>
The SDK accepts renderToken. The iframe sends X-Render-Token for metadata and rendering, taking precedence over any API-key/editor-token attributes. The token carries its tenant; no separate tenant header is needed. Fixed data is merged by the server after browser input. See Browser render tokens for mint/revoke examples and deployment settings.
For same-origin form iframes, omit the optional allowedOrigins restriction: browsers normally omit Origin on same-origin metadata GETs, and an origin-bound token rejects requests without Origin. Cross-origin API calls must send an allowed Origin and need CORS configuration. Render tokens cannot call parameter-resolution endpoints; v1 public forms should use an inputSchema without dynamic parameters or field mappings. A backend proxy remains an option for those advanced flows.
Raw api-key is available for trusted internal pages only. Editor tokens cannot call production /render endpoints. Keep minting API keys on the backend.
Server Configuration
The form embed uses the same CSP, CORS, and embedding rules as the editor. If you have already configured EMBED_ALLOWED_ORIGINS and CORS_ALLOWED_ORIGINS for <pulp-engine-editor>, no additional server configuration is needed. See Server Configuration above.
Attributes
| Attribute | Required | Description |
|---|---|---|
api-url |
Yes | Base URL of the Pulp Engine API server (or your proxy). Used for both metadata and render calls. |
template-key |
Yes | Template key to generate a form for. |
render-token |
No* | Scoped browser render token with allowFormMetadata: true; sent as X-Render-Token. |
api-key |
No* | Admin or render-scoped API key. Sent as X-Api-Key. Trusted internal pages only. |
token |
No* | Editor token. Sent as X-Editor-Token. Kept for forward compatibility; cannot call /render in v1. |
embed-url |
No | Override the origin that serves /form.html and /pulp-engine-form.js (for split-origin proxy deployments). Defaults to api-url. |
version |
No | Pin a specific template version. Mutually exclusive with label. |
label |
No | Resolve a named label (e.g. stable, prod). Mutually exclusive with version. |
tenant-id |
No | Multi-tenant slug, forwarded as X-PulpEngine-Tenant-Id. |
result-mode |
No | download (default) or blob. Controls how the generated output is delivered. |
submit-label |
No | Override the default “Generate PDF” button label. |
theme |
No | light (default) or dark. |
locale |
No | UI language (en, de, fr, ar) and selection of authored form-language variants. Updates live without losing answers. |
hidden-fields |
No | JSON object literal. Values merged into the submission and hidden from the UI. Example: hidden-fields='{"locale":"en-AU","channel":"web"}' |
* Either api-key or token must be provided on authenticated servers.
Events
Standard DOM CustomEvents dispatched on the <pulp-engine-form> element:
| Event | detail fields |
Description |
|---|---|---|
ready |
hasFieldMappings |
Metadata fetched and form rendered. hasFieldMappings: true means the template refused to render — see v1 Limitation. |
change |
— | User edited a field. Fires on every form input. |
submit |
— | User clicked submit; render request dispatched. |
success |
contentType, filename, size, blob? |
Render completed. blob is populated only when result-mode="blob". |
error |
code, message, status? |
Metadata fetch, validation, or render failed. |
Error codes
| Code | Meaning |
|---|---|
unsupported_mapped_template |
Template has fieldMappings; form embed refuses to render it (v1). |
unsupported_schema_root |
Template inputSchema is not an object with properties. |
metadata_fetch_failed |
GET /templates/:key/form-metadata returned non-2xx. Check status for HTTP status and message for the server detail. |
network_error |
Fetch threw before a response arrived (offline, DNS, CORS preflight). |
| Server render codes | On render failure, the server’s own code is forwarded verbatim — e.g. validation_failed, asset_blocked, template_expression_error. See Render execution error codes. |
Result modes
download(default) — the iframe creates an in-iframe<a download>and clicks it, kicking off a browser download directly from the iframe’s origin. The host page receives a lightweightsuccessevent withcontentType,filename, andsize. NoBlobis transferred.blob— the iframe posts the fullBlobback to the host via structured clone. The host owns the URL lifecycle (typicallyURL.createObjectURL+URL.revokeObjectURL). Use this when the host needs to inline the PDF into its own UI (e.g. a viewer, an attachment flow) rather than dropping it into the user’s Downloads folder.
v1 Limitation: field mappings
Templates with fieldMappings are refused by the form embed in v1. The ready event fires with hasFieldMappings: true and an error event with code: 'unsupported_mapped_template' is also dispatched; the form shows an inline message and no submit button.
Why: a template’s inputSchema describes the post-mapping data shape (what the template body’s {{…}} references). fieldMappings is a one-way transform from raw submission → post-mapping shape. The v1 form only knows the post-mapping schema, so it cannot honestly generate a form matching what the template expects end users to submit.
Detecting at build time: call GET /templates/:key/form-metadata before you render the embed and check hasFieldMappings. If true, either (a) author a companion template without mappings that produces the same PDF, or (b) build a custom form on your side that submits directly to /render/pdf.
Supported JSON Schema subset
The v1 form renders templates whose inputSchema uses these keywords. Anything outside this subset is either ignored or rendered as a generic free-text field.
- Types:
string,number,integer,boolean,object,array - Common:
required,enum,default,title,description - String:
minLength,maxLength,pattern,format(email,uri,date,date-time) - Number:
minimum,maximum,multipleOf - Array:
items(primitive item schemas),minItems,maxItems - Object: nested
properties,required[]
Out of scope in v1: oneOf / anyOf / allOf, if / then / else, $ref, patternProperties, dependencies, tuple items. Client-side validation is best-effort; the server re-validates the submission on /render/pdf and surfaces violations via the error event.
TypeScript SDK (@pulp-engine/form-embed)
import { createForm, loadFormScript } from '@pulp-engine/form-embed'
await loadFormScript({ embedUrl: 'https://pulp-engine.internal' })
const form = createForm({
apiUrl: 'https://pulp-engine.internal',
templateKey: 'invoice-v2',
apiKey: adminKey, // trusted internal pages only
resultMode: 'blob',
submitLabel: 'Generate invoice',
hiddenFields: { channel: 'web' },
})
form.on('ready', ({ hasFieldMappings }) => {
if (hasFieldMappings) {
alert('This template is not supported by the form embed.')
}
})
form.on('success', ({ blob, filename }) => {
const url = URL.createObjectURL(blob!)
window.open(url, '_blank')
setTimeout(() => URL.revokeObjectURL(url), 60_000)
})
form.on('error', ({ code, message, status }) => {
console.error(`[${code}] ${message}`, status)
})
form.mount(document.getElementById('form-container')!)
loadFormScript is idempotent — multiple calls resolve immediately once the element is registered.
Framework Examples
React
import { useEffect, useRef } from 'react'
import { createForm, loadFormScript } from '@pulp-engine/form-embed'
function InvoiceForm({ templateKey, apiKey }: { templateKey: string; apiKey: string }) {
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
let form: ReturnType<typeof createForm> | null = null
loadFormScript({ embedUrl: 'https://pulp-engine.internal' }).then(() => {
form = createForm({
apiUrl: 'https://pulp-engine.internal',
templateKey,
apiKey,
resultMode: 'download',
})
form.mount(containerRef.current!)
})
return () => { form?.unmount() }
}, [templateKey, apiKey])
return <div ref={containerRef} style={{ width: '100%', height: '600px' }} />
}
Vue 3
<template>
<div ref="container" style="width: 100%; height: 600px" />
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { createForm, loadFormScript } from '@pulp-engine/form-embed'
const props = defineProps<{ templateKey: string; apiKey: string }>()
const container = ref<HTMLElement>()
let form: ReturnType<typeof createForm> | null = null
onMounted(async () => {
await loadFormScript({ embedUrl: 'https://pulp-engine.internal' })
form = createForm({
apiUrl: 'https://pulp-engine.internal',
templateKey: props.templateKey,
apiKey: props.apiKey,
resultMode: 'download',
})
form.mount(container.value!)
})
onUnmounted(() => form?.unmount())
</script>
Vanilla HTML
<script src="https://pulp-engine.internal/pulp-engine-form.js"></script>
<pulp-engine-form
id="my-form"
api-url="https://pulp-engine.internal"
template-key="invoice-v2"
api-key="your-render-key"
result-mode="download"
style="width: 100%; height: 600px;"
></pulp-engine-form>
<script>
document.getElementById('my-form').addEventListener('success', (e) => {
console.log('Downloaded', e.detail.filename)
})
</script>
The visual editor can author these constraints, primitive array items and x-ui: textarea, infer from preview data, import JSON Schema and edit custom schema shapes as JSON. See Input schema.
Inline templates, preview data and snapshots
apiAuth: 'sandbox-token' selects the hosted sandbox credential transport;
normal integrations use the default editor-token transport. createEditor
exposes setInlineTemplate(template), setPreviewData(data) and
requestSnapshot(): Promise<{ template, previewData }>. Subscribe to snapshot
and snapshot-error for observation. Snapshot requests also reject on timeout
or if the iframe is disconnected. Mint tokens on the backend and supply their
tokenExpiresAt value alongside token.
Calculated mappings retain the existing hasFieldMappings signal. The form embed continues to reject templates containing field mappings, including additive computed mappings; use the editor embed to author them. GET /capabilities now advertises expressions.functions for compatibility checks.
Report options (Wave 37)
<pulp-engine-form> renders a Report options fieldset from safe form-metadata
parameter descriptors, resolving defaults and cascading options with
POST /templates/:key/parameters/resolve using the same credential/tenant headers
and version/label pin as metadata. Render submits { template, data, parameters }.
The ready message adds optional hasParameters for compatibility with existing
hosts. A mapped template with declared parameters gets a parameters-only form;
its data contains the configured hidden fields. Mapped templates without
parameters retain the unsupported-template response. Hidden parameters are not
prompted; internal parameters are never exposed. Choice labels, multi-select
checkboxes, select-all and date/range presets share the editor’s form controls.
The production-bundle embed checks run with pnpm --dir apps/editor test:e2e:embed after the usual API/editor builds. They start an isolated API and Chromium, covering white-label view-only behavior and render-token forms.
Guided interviews
<pulp-engine-form format="pdf|docx|xlsx|csv|pptx|html"> routes to the matching render endpoint and filename. Server capabilities and template support apply. Existing flat v1 schemas and "x-ui":"textarea" remain supported.
Root "x-ui-steps":[{"id":"applicant","title":"Applicant"},{"id":"details","title":"Details"},{"id":"confirm","title":"Confirm"}] defines steps. Property "x-ui":{"step":"details"} assigns a step; others use the first. Next validates visible current-step fields; a final Review page precedes submission. preview="true" adds a PDF preview on review.
Conditions support draft-07 if/then/else with const/enum predicates, array-form dependencies, and x-ui.visibleWhen. A visibility leaf is {"field":"business","op":"truthy"}. Operators: eq, neq, in, gt, lt, truthy, falsy, with nested all/any groups and root answer paths. Hidden fields are pruned before submission. Browser and server waive missing hidden-field requirements, while supplied values retain server type/value validation. Visibility is presentation/validation metadata, not authorization. Advanced references, combinators and tuple items still need custom forms.
Uploads and signatures
Set "x-ui":{"widget":"image"} or "file" on a string. PNG/JPEG/WebP magic bytes are checked and re-encoded; SVG is rejected. Defaults are 262144 bytes and 1600 px; maxBytes/maxDimension configure limits. Aggregate payloads above 900 KiB fail inline.
"x-ui":{"widget":"signature"} offers pointer drawing, Clear and a keyboard typed-name alternative, producing a PNG data URI. Bind through an image node’s src: "{{signature}}". Captured marks are not cryptographic signatures. readonly displays a preset/default and preserves it in submission.
Drafts
draft="local" saves {step,data} under pe-form:{templateKey}:{version}:{tenantId} and restores it on reload, tolerating storage failures. draft="host" emits debounced draft events; restore with initial-data/initial-step or wrapper options initialData/initialStep. Files/signatures are omitted unless draft-include-files="true". Host drafts work where iframe storage is unavailable. Protect saved answers according to their sensitivity.
Options and localisation
Lookups use "x-ui":{"widget":"lookup","lookup":{"source":"host:accounts","valuePath":"id","labelPath":"name"}}. Call element.setOptions('accounts', [{id:'AC-1',name:'Account One'}]); options replay on ready. Only the configured parent window and origin can supply options. URL sources require exact origins in server-provided EMBED_CONNECT_ORIGINS (same-origin also allowed), omit credentials, reject redirects, time out after 15 seconds and cap responses at 1 MiB/1000 items.
locale="fr" selects French UI messages and matching authored field text from
x-ui.i18n.fr. Schema titles, help, placeholders and enum labels remain authored
data; the UI catalogue does not translate them automatically. Object-form x-ui
supports these authored variants.
messages='{"next":"Suivant","required":"est obligatoire"}' overrides the
active UI catalogue with host wording. An explicit submit-label takes precedence
for the submit button. Public wrapper options mirror these attributes. Mapped
templates use typed parameters when available; otherwise provide a custom form.
UI language
English is the fallback. German, French and Arabic ship as Beta pending native
review. Non-English catalogues load on demand. Language selection resolves the
host’s locale first, then the saved editor preference, then
navigator.languages, then English. Unsupported host languages use English.
The standalone editor offers a Language menu next to the theme selector.
const editor = createEditor({
apiUrl, templateKey: 'invoice', token,
locale: 'fr', localeSelectable: true,
})
editor.setLocale('ar')
editor.setLocale(null) // Remove the host override.
const form = createForm({apiUrl, templateKey: 'invoice', renderToken, locale: 'de'})
form.setLocale('fr')
Both custom elements observe their locale attribute. Changing or removing it
preserves the iframe, template edits and form answers. React wrappers accept live
locale props; the editor wrapper also accepts localeSelectable. The form SDK’s
setLocale(locale) accepts null to remove the override.
Raw integrations may include locale?: string in either init message and
localeSelectable?: boolean in the editor init message. Runtime messages are
{type:'pulp-engine:set-locale', locale:'ar', localeSelectable:false} for the editor
and {type:'pulp-engine-form:set-locale', locale:'de'} for the form. A null locale
clears the host override. Locale values must be strings of at most 64 characters
or null for a runtime reset. Existing origin and source-window checks apply.
The iframe document gets the active lang and dir; Arabic mirrors chrome and
Radix menus. Authored document direction and renderConfig.locale remain
independent, and preview frames keep their document coordinates. Host-page
lang and dir are not modified. UI-language changes do not reload form metadata
or translate template content and plugin labels. Server error details remain in
their original language; classified UI errors and local validation are localised.