Skip to main content
Version: 0.0.2

Headless Runners (Script Runners)

A headless runner is a sandboxed extension surface that runs in a hidden iframe — no visible UI, no navigation panel, no toolbar button. It has full access to the same window.kittl SDK as panel extensions and is designed for background or automated workflows.

Use Cases​

  • AI generation pipelines — register a generator handler, trigger generation, receive streaming status updates without UI
  • Realtime collaboration — subscribe to stateless messages from peers, process data in the background
  • Data synchronization — read/write design elements programmatically based on external triggers
  • Background processing — any automated workflow that doesn't need user interaction

Manifest Configuration​

Extensions declare a headless runner via config.embed.scriptRunner in manifest.json:

{
"config": {
"scopes": ["design:state:read", "design:state:write", "ai:credit:spend"],
"embed": {
"mainAppPanel": {
"path": "index.html"
},
"scriptRunner": {
"path": "runner.html"
}
}
}
}

Key points:

  • scriptRunner is optional — extensions don't need one.
  • mainAppPanel is the visible UI panel; scriptRunner is the headless background runner.
  • Both can coexist — an extension can have both surfaces (or just one).
  • path is relative to your built assets (same as mainAppPanel.path).

Creating a Runner​

A runner entry point is a plain HTML file that loads the SDK and runs code on ready. There is nothing to render — the iframe is always hidden.

runner.html
<!doctype html>
<html>
<head>
<script src="https://sdk.kittl.com/kittl-sdk.esm.js" type="module"></script>
</head>
<body>
<script type="module">
window.kittl.onReady(async () => {
console.log('Headless runner is ready');

// Example: register as an AI generation handler
const result = await kittl.ai.registerHandler(
'my-generator',
(update) => {
console.log('Generation status:', update.status);
if (update.status === 'COMPLETE') {
console.log('Result:', update.result);
}
},
);

if (result.isOk) {
console.log('Handler registered');
}
});
</script>
</body>
</html>

SDK Access​

The runner has access to the exact same window.kittl API as a panel extension:

All API calls go through the same scope enforcement. If the manifest declares ["design:state:read"], the runner can only read — it cannot write.

Communicating Between Panel and Runner​

Use the Stateless Messaging API to communicate between a panel extension and its headless runner. Both surfaces share the same extension ID, so messages are automatically scoped.

Panel (index.html)
// Send a command to the runner
await kittl.stateless.send({
type: 'generate-request',
payload: { prompt: 'A sunset over mountains' },
});
Runner (runner.html)
// Listen for commands from the panel
await kittl.stateless.subscribe('generate-request', (payload) => {
const prompt = payload.prompt as string;
// Start generation with the received prompt…
});

Lifecycle​

  • The runner activates when the extension loads, alongside any main panel.
  • It persists as long as the extension is installed and the editor is open.
  • It is destroyed when the editor unmounts or the extension is uninstalled.

Scoping​

Scope enforcement is identical to panel extensions. Declare the required scopes in manifest.json under config.scopes. See SDK Scopes for the full list.