Requirement Sheets
Requirement sheets pause tool execution and show an inline form at the top of the conversation. Use them when your extension tool needs something from the user before it can run -- an API key, OAuth login, or configuration setting.
Related:
- Extensions API -- Full API reference
- Extensions UI Library -- CSS variables and components
How It Works
- The AI model calls your extension tool
- Your tool handler detects a missing prerequisite (no API key, expired token, etc.)
- Your handler returns an error with a
requirementSheetobject containing HTML - Chatons shows the sheet as an iframe at the top of the conversation
- The user fills in the form and clicks "Save" (or "Cancel")
- The sheet iframe uses
postMessageto signal back to the parent - Chatons closes the sheet and returns the tool result to the model
The tool error has already been returned to the model by the time the sheet appears. The model should instruct the user to retry after fixing the issue.
⚠️ Important: The sheet iframe is sandboxed and cannot directly call parent functions. Always use postMessage to communicate with the parent window (see section below).
Returning a Requirement Sheet
When your tool handler detects missing prerequisites, return an error with a requirementSheet property:
window.addEventListener('message', function (event) {
var data = event && event.data;
if (!data || data.type !== 'chaton.extension.apiCall') return;
if (data.apiName !== 'my_tool') return;
// Check for prerequisite
var apiKey = getStoredApiKey();
if (!apiKey) {
window.parent.postMessage({
type: 'chaton.extension.apiCallResponse',
callId: data.callId,
result: {
ok: false,
error: {
code: 'unauthorized',
message: 'API key not configured. Please set it in the sheet above.',
requirementSheet: {
title: 'Configure API Key',
html: buildConfigSheetHtml(),
},
},
},
}, '*');
return;
}
// Normal execution
window.parent.postMessage({
type: 'chaton.extension.apiCallResponse',
callId: data.callId,
result: { ok: true, data: { result: 'success' } },
}, '*');
});
requirementSheet object
| Property | Type | Required | Description |
|---|---|---|---|
title | string | No | Title shown in the sheet header. Defaults to "Action Required". |
html | string | Yes | Full HTML document rendered in the sheet iframe. |
Sheet HTML
The sheet is rendered as a sandboxed iframe (allow-scripts allow-same-origin allow-forms). This iframe runs in an isolated context, separate from the parent window.
⚠️ Critical: Use postMessage for Communication
Because the iframe is sandboxed, you cannot call functions on window.chaton.requirementSheet or other parent objects directly. The iframe has no direct access to parent APIs.
Always use postMessage to communicate:
// ✅ CORRECT - This works in a sandboxed iframe
window.parent.postMessage({ type: 'chaton:requirement-sheet:confirm' }, '*');
// ❌ WRONG - This will fail silently
window.chaton.requirementSheet.confirm(); // undefined
Available communication methods
Using postMessage (recommended):
// Signal that the user completed all required actions
window.parent.postMessage({ type: 'chaton:requirement-sheet:confirm' }, '*');
// Dismiss the sheet without completing
window.parent.postMessage({ type: 'chaton:requirement-sheet:dismiss' }, '*');
// Open Chatons Settings and dismiss the sheet
window.parent.postMessage({ type: 'chaton:requirement-sheet:open-settings' }, '*');
Note: The iframe is sandboxed and runs in a separate context, so direct function calls like window.chaton.requirementSheet.confirm() are not available. Always use postMessage to communicate with the parent window.
Complete Example
Manifest
{
"id": "@user/chatons-weather",
"name": "Weather Extension",
"version": "1.0.0",
"capabilities": ["llm.tools", "storage.kv"],
"llm": {
"tools": [
{
"name": "get_weather",
"description": "Get current weather for a city.",
"parameters": {
"type": "object",
"properties": {
"city": { "type": "string" }
},
"required": ["city"]
}
}
]
},
"apis": {
"exposes": [
{ "name": "get_weather", "version": "1.0.0" }
]
}
}
Tool handler with requirement sheet
var EXTENSION_ID = '@user/chatons-weather';
window.addEventListener('message', function (event) {
var data = event && event.data;
if (!data || data.type !== 'chaton.extension.apiCall') return;
if (data.apiName !== 'get_weather') return;
// Check for stored API key
window.chaton.extensionStorageKvGet(EXTENSION_ID, 'weather_api_key')
.then(function (result) {
var apiKey = result && result.ok && result.data;
if (!apiKey) {
// Return requirement sheet
window.parent.postMessage({
type: 'chaton.extension.apiCallResponse',
callId: data.callId,
result: {
ok: false,
error: {
code: 'unauthorized',
message: 'Weather API key required. A configuration sheet has been shown.',
requirementSheet: {
title: 'Set up Weather API Key',
html: buildWeatherKeySheet(),
},
},
},
}, '*');
return;
}
// Normal execution with the API key
fetch('https://api.weather.example.com/v1/current?city=' + encodeURIComponent(data.payload.city), {
headers: { 'Authorization': 'Bearer ' + apiKey }
})
.then(function (resp) { return resp.json(); })
.then(function (weather) {
window.parent.postMessage({
type: 'chaton.extension.apiCallResponse',
callId: data.callId,
result: { ok: true, data: weather },
}, '*');
});
});
});
function buildWeatherKeySheet() {
return [
'<!DOCTYPE html>',
'<html><head><meta charset="utf-8">',
'<style>',
' body { font-family: -apple-system, BlinkMacSystemFont, sans-serif;',
' padding: 20px 24px; margin: 0;',
' background: var(--chaton-ui-background, #f5f6f8);',
' color: var(--chaton-ui-foreground, #1a1b22); font-size: 14px; }',
' .field { margin-bottom: 16px; }',
' label { display: block; font-weight: 500; font-size: 13px; margin-bottom: 6px; }',
' input { width: 100%; box-sizing: border-box; padding: 8px 12px;',
' border-radius: 8px; border: 1px solid var(--chaton-ui-border, #d6d9e2);',
' background: var(--chaton-ui-card, #fff); color: inherit; font: inherit; }',
' input:focus { outline: 2px solid var(--chaton-ui-ring, #6b7280); outline-offset: 2px; }',
' .actions { display: flex; gap: 10px; justify-content: flex-end; }',
' button { font: inherit; font-size: 13px; font-weight: 500; padding: 8px 18px;',
' border-radius: 8px; cursor: pointer;',
' border: 1px solid var(--chaton-ui-border, #d6d9e2);',
' background: var(--chaton-ui-card, #fff); color: inherit; }',
' .btn-primary { background: var(--chaton-ui-primary, hsl(220 7% 32%));',
' color: var(--chaton-ui-primary-foreground, #fff);',
' border-color: var(--chaton-ui-primary, hsl(220 7% 32%)); }',
' @media (prefers-color-scheme: dark) {',
' body { background: hsl(222 14% 12%); color: hsl(210 20% 90%); }',
' input { background: hsl(222 13% 17%); border-color: hsl(222 10% 24%); }',
' button { background: hsl(222 13% 17%); border-color: hsl(222 10% 24%); }',
' }',
'</style>',
'</head><body>',
' <div class="field">',
' <label for="api-key">Weather API Key</label>',
' <input id="api-key" type="password" placeholder="Enter your API key" />',
' </div>',
' <p style="font-size:12px;color:var(--chaton-ui-muted-foreground,#6b7280);margin-bottom:16px">',
' Your key is stored locally and never shared.',
' </p>',
' <div class="actions">',
' <button onclick="dismissSheet()">Cancel</button>',
' <button class="btn-primary" onclick="saveKey()">Save</button>',
' </div>',
' <script>',
' function saveKey() {',
' var key = document.getElementById("api-key").value.trim();',
' if (!key) return;',
' window.parent.postMessage({ type: "chaton:requirement-sheet:confirm" }, "*");',
' }',
' function dismissSheet() {',
' window.parent.postMessage({ type: "chaton:requirement-sheet:dismiss" }, "*");',
' }',
' </script>',
'</body></html>',
].join('\n');
}
Built-in Auth Error Sheets
Chatons automatically shows a requirement sheet when an LLM provider returns an authentication error. This handles:
- 401 Unauthorized responses
- Expired OAuth tokens
- Invalid or missing API keys
- 403 Forbidden responses
The built-in sheet shows the error message and offers:
- Dismiss -- Close the sheet
- Open Settings -- Navigate to Settings > Providers & Models
No extension code is needed. This is built into the conversation runtime.
Styling
Use CSS variables from the Chatons design system for a native look:
| Variable | Purpose |
|---|---|
--chaton-ui-background | Page background |
--chaton-ui-foreground | Text color |
--chaton-ui-card | Card/input background |
--chaton-ui-border | Border color |
--chaton-ui-primary | Primary button background |
--chaton-ui-primary-foreground | Primary button text |
--chaton-ui-muted-foreground | Muted text color |
--chaton-ui-ring | Focus ring color |
Dark mode: Use @media (prefers-color-scheme: dark) or rely on the CSS variables (they automatically adjust in dark mode).
Max height: Sheets have a max height of approximately 360px. Keep content compact.
User Interaction
| Action | Result |
|---|---|
| Click close icon (X) | Sheet dismissed |
| Click outside the sheet | Sheet dismissed |
| Press Escape | Sheet dismissed |
confirm() | Sheet confirmed and closed |
dismiss() | Sheet dismissed |
openSettings() | Opens Settings and closes sheet |
Common Mistakes
Mistake 1: Trying to call parent functions directly
// ❌ WRONG - Will not work (iframe is sandboxed)
button.onclick = () => {
window.chaton.requirementSheet.confirm();
};
// ✅ CORRECT - Use postMessage instead
button.onclick = () => {
window.parent.postMessage({ type: 'chaton:requirement-sheet:confirm' }, '*');
};
Mistake 2: Forgetting to check iframe context
The sheet runs in a separate iframe, so window.chaton is undefined. Always assume you're in a sandboxed context and use postMessage.
Mistake 3: Using event handlers without proper closure
// ❌ Problem: event.data might be stale
window.addEventListener('input', function(event) {
button.onclick = () => {
var value = input.value; // References outer scope
window.parent.postMessage({ type: 'confirm', value }, '*');
};
});
// ✅ Better: Capture value directly
button.onclick = function() {
var value = input.value;
window.parent.postMessage({ type: 'chaton:requirement-sheet:confirm' }, '*');
};
Constraints
- Only one sheet can be active per conversation at a time
- The tool error has already been returned to the model before the sheet appears
- The sheet iframe is sandboxed (
allow-scripts allow-same-origin allow-forms) - The sandboxed iframe cannot access parent APIs directly — use
postMessageinstead - The sheet does not have direct access to the parent extension's
window.chatonAPIs (it runs in a separate iframe context)