FPO Templates

An FPO template is a wrapper around the canonical Full Product Object (FPO) format. It lets you publish a reusable product definition while leaving specific values for the importer to fill in later.

Use an FPO template when:

  • the product structure is stable
  • the deployer needs to supply names, URLs, API keys, or environment-specific values
  • you want a /now preview before the product is created

Template Shape

An FPO template has three top-level keys:

  • version: the template document format version. Current value: 1.0
  • productObject: a normal Full Product Object
  • template.variables: metadata for every variable referenced inside the FPO

The nested productObject must also declare its own version ("1.0", "1.1", or "2.0"; "2.0" is the current default). At "2.0" (the default), each inline agent’s runtime fields (model, systemPrompt, tools, …) live under agent.config; at "1.0"/"1.1" they sit directly on the agent object.

Variable references are allowed in string leaves inside the FPO:

1"name": "{{productName}}"

The full example used in this guide is available at docs/templates/quick-start/customer-support-fpo-template.json.

Variable Manifest

Each variable must be declared in template.variables with:

FieldRequiredNotes
keyYesReferenced by {{key}} inside productObject
labelYesUser-facing form label
descriptionNoShown in preview UIs
inputTypeYesOne of text, textarea, url, secret, select, number, boolean
requiredYesWhether the importer must supply a value
defaultValueNoAllowed for non-secret variables only
placeholderNoHelpful UI hint
optionsSelect onlyRequired when inputType is select

Resolution Rules

  • Every {{variableName}} reference must be declared in the manifest
  • Inline defaults such as {{apiKey|default}} are not supported
  • Defaults must be defined with template.variables[].defaultValue
  • Secret variables cannot define defaults
  • If a field value is exactly {{variableName}} and the variable type is number or boolean, the resolved value keeps that scalar type
  • Otherwise, substitutions resolve to strings

Example Template

1{
2 "version": "1.0",
3 "productObject": {
4 "version": "2.0",
5 "product": {
6 "name": "{{productName}}",
7 "description": "Support automation for {{companyName}}",
8 "metadata": {
9 "requireEscalationApproval": "{{requireEscalationApproval}}",
10 "responseTimeoutMinutes": "{{responseTimeoutMinutes}}"
11 }
12 },
13 "capabilities": [
14 {
15 "id": "cap_support",
16 "name": "Support Agent",
17 "description": "Handle incoming support requests.",
18 "agent": {
19 "name": "{{productName}} Agent",
20 "description": "Handle support requests for {{companyName}}.",
21 "config": {
22 "model": "claude-sonnet-4-5",
23 "systemPrompt": "Use a {{brandVoice}} tone when responding."
24 }
25 }
26 }
27 ],
28 "tools": [
29 {
30 "id": "tool_search",
31 "type": "integration",
32 "provider": "{{searchProvider}}",
33 "name": "Knowledge Search",
34 "config": {
35 "apiKey": "{{searchApiKey}}",
36 "baseUrl": "{{knowledgeBaseUrl}}"
37 },
38 "auth": {
39 "type": "user_provided",
40 "setupRequired": true,
41 "secrets": [
42 {
43 "key": "searchApiKey",
44 "required": true
45 }
46 ],
47 "setupInstructions": {
48 "summary": "Add your search provider key",
49 "steps": ["Create an API key", "Paste it into the deployment form"]
50 }
51 }
52 }
53 ],
54 "surfaces": [
55 {
56 "id": "surface_chat",
57 "name": "{{productName}} Chat",
58 "type": "chat",
59 "config": {},
60 "routes": [
61 {
62 "capabilityId": "cap_support"
63 }
64 ]
65 }
66 ],
67 "_meta": {
68 "schemaVersion": "2.0",
69 "catalogVersion": "1.0",
70 "generatedAt": "2026-03-07T00:00:00.000Z",
71 "generatorVersion": "1.0.0",
72 "planHash": "template-example"
73 }
74 },
75 "template": {
76 "variables": [
77 {
78 "key": "productName",
79 "label": "Product Name",
80 "inputType": "text",
81 "required": true,
82 "defaultValue": "Acme Support Copilot"
83 },
84 {
85 "key": "companyName",
86 "label": "Company Name",
87 "inputType": "text",
88 "required": true
89 },
90 {
91 "key": "knowledgeBaseUrl",
92 "label": "Knowledge Base URL",
93 "inputType": "url",
94 "required": true
95 },
96 {
97 "key": "searchProvider",
98 "label": "Search Provider",
99 "inputType": "select",
100 "required": true,
101 "defaultValue": "firecrawl",
102 "options": [
103 { "label": "Firecrawl", "value": "firecrawl" },
104 { "label": "Exa", "value": "exa" }
105 ]
106 },
107 {
108 "key": "requireEscalationApproval",
109 "label": "Require Escalation Approval",
110 "inputType": "boolean",
111 "required": true,
112 "defaultValue": true
113 },
114 {
115 "key": "responseTimeoutMinutes",
116 "label": "Response Timeout Minutes",
117 "inputType": "number",
118 "required": true,
119 "defaultValue": 15
120 },
121 {
122 "key": "brandVoice",
123 "label": "Brand Voice",
124 "inputType": "textarea",
125 "required": false,
126 "defaultValue": "Calm and direct."
127 },
128 {
129 "key": "searchApiKey",
130 "label": "Search API Key",
131 "inputType": "secret",
132 "required": true
133 }
134 ]
135 }
136}

Scheduled jobs in templates

Use the top-level productObject.schedules[] array for executable cron or one-time automation. A schedule points directly at the capability that should run through capabilityId; importing the FPO creates the schedule record.

Do not add a type: "schedule" surface just to make a scheduled capability reachable. Schedule surfaces are optional management or presentation containers; they are not the trigger by themselves, and the validator counts schedules[].capabilityId as a reachable capability.

1{
2 "productObject": {
3 "capabilities": [
4 {
5 "id": "cap_support",
6 "name": "Support Agent",
7 "description": "Answer user questions.",
8 "existingAgentId": "agent_support"
9 },
10 {
11 "id": "cap_weekly_digest",
12 "name": "Weekly Digest",
13 "description": "Summarize new records each Monday.",
14 "existingFlowId": "flow_weekly_digest"
15 }
16 ],
17 "surfaces": [
18 {
19 "id": "surface_chat",
20 "name": "Support Chat",
21 "type": "chat",
22 "config": {},
23 "routes": [{ "capabilityId": "cap_support" }]
24 }
25 ],
26 "schedules": [
27 {
28 "id": "weekly-digest",
29 "capabilityId": "cap_weekly_digest",
30 "triggerType": "cron",
31 "cron": "0 9 * * MON",
32 "timezone": "UTC",
33 "enabled": true
34 }
35 ]
36 }
37}

Use a schedule surface only when the product needs a dedicated surface for users or operators to inspect and manage schedule-related behavior.

Eval suites in templates

Use the optional top-level productObject.evals[] array to ship eval suites with a product. Each suite points at the capability it tests through capabilityId; importing the FPO creates the suite, its graders, and its example cases alongside the capability’s flow or agent.

FieldRequiredNotes
idYesTemplate-local suite id, used for duplicate detection
nameYesSuite display name
capabilityIdYesReferences a capabilities[].id; the suite targets that capability’s flow or agent
gradersYesAt least one grader (max 20). Uses the shared grader contract: deterministic checks such as contains or json_field, trace checks such as called_tool, and AI judges (kind: "ai")
casesNoExample cases (max 200). Each case has a name, optional input.variables or input.messages, an optional expected block (text, json, or facts), plus notes and enabled
recordedToolModeNoHow a captured case replays the target’s recorded tool calls: next_step or continue
recordedToolUnmatchedPolicyNoWhat happens when a replayed run makes a tool call with no recording: fail or stub
1{
2 "productObject": {
3 "capabilities": [
4 {
5 "id": "cap_support",
6 "name": "Support Agent",
7 "description": "Answer user questions.",
8 "existingAgentId": "agent_support"
9 }
10 ],
11 "evals": [
12 {
13 "id": "eval_support_basics",
14 "name": "Support basics",
15 "capabilityId": "cap_support",
16 "graders": [
17 {
18 "kind": "ai",
19 "criteria": "The response directly addresses what the user asked, without dodging or answering a different question."
20 }
21 ],
22 "cases": [
23 {
24 "name": "Refund policy question",
25 "input": {
26 "messages": [{ "role": "user", "content": "What is your refund policy?" }]
27 },
28 "expected": { "facts": ["Refunds are available within 30 days"] },
29 "enabled": true
30 }
31 ]
32 }
33 ]
34 }
35}

Re-importing or converging a product replaces only the cases that originally came from the FPO. Cases you author manually or save from a run in the dashboard are preserved, and those platform-authored cases are not exported back into the FPO when you pull the product definition.

See What are Evals? for how graders and cases work, and Managing eval suites for editing imported suites in the dashboard.

Skills in templates

Use the optional top-level productObject.skills[] array to ship agent skills with a product. A skill is a loadable context bundle: a markdown body plus optional capabilities the agent activates when it loads the skill. Importing the FPO publishes each skill and binds it to the agents listed in bindTo.

FieldRequiredNotes
idYesTemplate-local skill id, used for duplicate detection
nameYesSkill display name
descriptionYesWhen the agent should load the skill
contentYesThe SKILL.md markdown body loaded into the agent’s context when the skill fires
slugNoLowercase identifier (starts with a letter; letters, digits, underscores, and hyphens; max 64 chars)
trustLevelNoOne of org, imported, or community
capabilitiesNoTools the skill activates: capabilityRefs (FPO capability ids, resolved to the backing flow or agent), toolIds (saved tool ids), and inlineTools
bindToNoFPO capability ids of agent-backed capabilities the published skill is bound to

A skill’s capabilities cannot declare mcpServers. The key is rejected at validation time with an explicit error rather than silently dropped; bind MCP tools through a saved tool id or an inline tool instead.

1{
2 "productObject": {
3 "capabilities": [
4 {
5 "id": "cap_support",
6 "name": "Support Agent",
7 "description": "Answer user questions.",
8 "existingAgentId": "agent_support"
9 }
10 ],
11 "skills": [
12 {
13 "id": "skill_refund_policy",
14 "name": "Refund policy",
15 "slug": "refund-policy",
16 "description": "Load when the user asks about refunds, returns, or exchanges.",
17 "content": "# Refund policy\n\nRefunds are available within 30 days of purchase. Always confirm the order number before promising a refund.",
18 "bindTo": ["cap_support"]
19 }
20 ]
21 }
22}

Validate a Template Before Publishing

Use POST /v1/public/products/validate-template before shipping a template to users.

cURL
$curl https://api.runtype.com/v1/public/products/validate-template \
> -H "Content-Type: application/json" \
> -d @docs/templates/quick-start/customer-support-fpo-template.json
TypeScript
1const template = await fetch('/customer-support-fpo-template.json').then((response) =>
2 response.json()
3)
4
5const validation = await fetch('https://api.runtype.com/v1/public/products/validate-template', {
6 method: 'POST',
7 headers: {
8 'Content-Type': 'application/json',
9 },
10 body: JSON.stringify(template),
11}).then((response) => response.json())
12
13console.log(validation.valid)
14console.log(validation.errors)
15console.log(validation.referencedVariableKeys)
16console.log(validation.defaultsSufficient)

The validation response reports:

  • structural template errors
  • undeclared variable references
  • unused manifest variables
  • normalized variable metadata
  • whether defaults are enough to produce a valid resolved FPO without extra input

Creation Workflow

Once a template validates:

  1. Preview it with POST /v1/quick-start/imports/preview
  2. Collect missing variable values from the preview response
  3. Create the product with POST /v1/quick-start/create

See Importing Products for the full import-session flow.

Secret variables are part of the manifest, but their values should only be provided at create time. Do not put live secrets into template files or default values.