FPO templates

An FPO template wraps a Full Product Object (FPO). Use it to publish a reusable product definition with values that you collect during import.

Use an FPO template when you need to:

  • Keep the product structure stable.
  • Collect names, URLs, API keys, or environment-specific values during import.
  • Preview the product with /now before you create it.

Template shape

The public FPO template contract uses these top-level keys:

  • version sets the template document format version. Use 1.0 or 1.1.
  • productObject contains the FPO.
  • template.variables contains metadata for each variable referenced in the FPO.

Set productObject.version to 1.0, 1.1, or 2.0. The 2.0 version is the default FPO version. In 2.0, place inline agent runtime fields under agent.config. In 1.0 and 1.1, place those fields directly on the agent object.

Use variable references in string values within the FPO:

Variable reference
1{
2 "name": "{{productName}}"
3}

The repository includes a complete example at docs/templates/quick-start/customer-support-fpo-template.json.

Variable manifest

Declare each variable in template.variables with the following fields:

FieldRequiredNotes
keyYesReference this key with {{key}} inside productObject.
labelYesUse this value as the form label.
descriptionNoShow this value in preview UIs.
inputTypeYesSet to text, textarea, url, secret, select, number, or boolean.
requiredYesSet whether the importer must supply a value.
defaultValueNoSet this only for non-secret variables.
placeholderNoProvide a hint for the input field.
optionsWhen selectProvide options when inputType is select.

Resolution rules

Resolve a template with these rules:

  • Declare every {{variableName}} reference in the manifest.
  • Do not use inline defaults such as {{apiKey|default}}.
  • Define defaults with template.variables[].defaultValue.
  • Do not define defaults for secret variables.
  • Preserve the number or boolean scalar type when a field value is exactly {{variableName}}.
  • Resolve every other substitution to a string.

Example template

Use this complete template as a starting point:

Full FPO 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}

Add scheduled jobs

Use the top-level productObject.schedules[] array for cron or one-time automation. Each schedule targets a capability through capabilityId. Importing the FPO creates the schedule record.

Do not add a type: "schedule" surface to make a scheduled capability reachable. A schedule surface is an optional management or presentation container, not the trigger. The validator counts schedules[].capabilityId as a reachable capability.

Use the following structure to define a scheduled capability:

Scheduled 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}

Add a schedule surface only when you need a dedicated surface for users or operators to inspect and manage schedule behavior.

Add eval suites

Use the optional top-level productObject.evals[] array to include eval suites in a product. Each suite targets a capability through capabilityId. Importing the FPO creates the suite, its graders, and its example cases with the capability’s flow or agent.

The following fields define an eval suite in an FPO:

FieldRequiredNotes
idYesSet a template-local suite ID for duplicate detection.
nameYesSet the suite display name.
capabilityIdYesReference a capabilities[].id for the flow or agent that the suite tests.
gradersYesAdd one to 20 graders. Use deterministic checks such as contains or json_field, trace checks such as called_tool, or an AI judge with kind: "ai".
casesNoAdd up to 200 example cases. Each case can include a name, input.variables, input.messages, an expected block, notes, and enabled.
recordedToolModeNoSet how a captured case replays recorded tool calls: next_step or continue.
recordedToolUnmatchedPolicyNoSet what happens when a replayed run makes an unrecorded tool call: fail or stub.

Use the following structure to add an eval suite:

Eval suite
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.

For grader and case concepts, see What are Evals?. Use Managing eval suites to edit imported suites in the dashboard.

Add skills

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

The following fields define a skill in an FPO:

FieldRequiredNotes
idYesSet a template-local skill ID for duplicate detection.
nameYesSet the skill display name.
descriptionYesDescribe when to load the skill.
contentYesProvide the SKILL.md markdown body that loads into the agent’s context when the skill fires.
slugNoUse a lowercase identifier that starts with a letter and contains letters, digits, underscores, or hyphens. Limit it to 64 characters.
trustLevelNoSet to org, imported, or community.
capabilitiesNoBind FPO capabilities with capabilityRefs, saved tools with toolIds, or inline tools with inlineTools.
bindToNoList FPO capability IDs for the agent-backed capabilities that the skill binds to.

Do not declare mcpServers in a skill’s capabilities. The validator rejects the key with an explicit error. Bind MCP tools with a saved tool ID or an inline tool.

Use the following structure to add a skill:

Skill definition
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

Validate a template with POST /v1/public/products/validate-template before you publish it.

Send the template from TypeScript with this code:

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 Authorization: `Bearer ${process.env.RUNTYPE_API_KEY}`,
9 'Content-Type': 'application/json',
10 },
11 body: JSON.stringify(template),
12}).then((response) => response.json())
13
14console.log(validation.valid)
15console.log(validation.errors)
16console.log(validation.referencedVariableKeys)
17console.log(validation.defaultsSufficient)

Send the template file to the validation endpoint with this cURL request:

cURL
$curl https://api.runtype.com/v1/public/products/validate-template \
> -H "Authorization: Bearer YOUR_API_KEY" \
> -H "Content-Type: application/json" \
> -d @docs/templates/quick-start/customer-support-fpo-template.json

Replace YOUR_API_KEY with a Runtype API key.

The validation response reports the following results:

  • Structural template errors.
  • Undeclared variable references.
  • Unused manifest variables.
  • Normalized variable metadata.
  • Whether defaults produce a valid resolved FPO without extra input.

Create a product from a template

After the template validates, follow these steps:

  1. Preview the template 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.

An inputType: "secret" variable is substituted literally into the resolved product object. Do not put live secrets in template files or defaultValue. Provide the value at creation time, or declare the credential in the target tool’s auth.secrets and reference it with {{ secret: KEY }}.

Next steps

Continue with these guides: