How extensions render as widgets inside other apps via named UI slots — the framework's optional extension system. Use when an extension-enabled app needs to wire an ExtensionSlot or make an existing one-off custom block installable into a slot.
65
79%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./.agents/skills/extension-points/SKILL.mdTerminology note. "Extensions" in this doc are the framework's sandboxed Alpine.js mini-app primitive (see the
extensionsskill). They are NOT LLM "tools" (function calls). The slot-system tables are still physically namedtool_slotsandtool_slot_installsfor back-compat — see the table at the bottom of this doc and the "Database & API names" section in theextensionsskill.
Slots are named React-shaped holes in apps. Extensions are widgets that opt into filling those holes. The framework matches them up by string ID.
Three primitives:
| Primitive | What it is |
|---|---|
| Slot | <ExtensionSlot id="..." context={...} /> dropped into an app's JSX |
| Slot target | A row saying "extension X can render in slot Y" — tool_slots table (Drizzle: extensionSlots) |
| Slot install | A row saying "user U wants extension X in slot Y" — tool_slot_installs (Drizzle: extensionSlotInstalls) |
Slots do not make extensions a default product surface. Most apps keep extension creation and discovery disabled while preserving installed blocks and old deep links for compatibility. Add a slot only when the app deliberately supports this customization seam; otherwise implement the requested behavior in native app code.
When <ExtensionSlot> renders, it queries the user's installs and mounts
one <EmbeddedTool> (a small auto-sized iframe) per install, pushing the
slot's context into each via postMessage. (The component is still exported
as EmbeddedTool for back-compat.)
<app>.<area>.<position> — three dot-separated lowercase-kebab segments.
mail.contact-sidebar.bottommail.thread-toolbar.actionsclips.right-panel.tabscalendar.event-detail.bottomStable strings. Renaming a slot is a data migration — same as renaming a route.
Only follow this flow when the host app explicitly enables extensions and the user has asked for a one-off custom block. Reusable behavior belongs in app code.
Create the extension with create-extension. The HTML can read
window.slotContext to get the host's context (the contact email,
recording id, etc.) and subscribe to changes via
window.onSlotContext(fn).
<div
x-data="{ contact: null }"
x-init="contact = window.slotContext; window.onSlotContext(c => contact = c)"
>
<template x-if="contact">
<div class="rounded-lg border p-4 m-4">
<p class="text-sm">
Notes for <span x-text="contact.contactEmail"></span>
</p>
</div>
</template>
</div>Declare the slot target with add-extension-slot-target:
add-extension-slot-target { extensionId: "<id>", slotId: "mail.contact-sidebar.bottom" }Install it for the current user with install-extension:
install-extension { extensionId: "<id>", slotId: "mail.contact-sidebar.bottom" }The slot will pick up the install on its next render (≤2s via polling sync, immediate after the action's UI invalidation).
Drop <ExtensionSlot> wherever you want to allow extensions:
import { ExtensionSlot } from "@agent-native/core/client/extensions";
// inside your component
<ExtensionSlot
id="mail.contact-sidebar.bottom"
context={{ contactEmail: contact.email, contactName: contact.name }}
showEmptyAffordance
/>;The legacy import path
@agent-native/core/client/toolscontinues to re-export the same component for back-compat with existing templates.
Props:
id — slot identifier. Must match what extensions target.context — object pushed to each embedded extension as slotContext. Re-pushed
whenever this prop changes.showEmptyAffordance — when true, shows a "+ Add widget" button in the
empty state. Default: false (slot renders nothing when empty).className / toolClassName — optional styling hooks. (The toolClassName
prop name is kept for back-compat; it styles the embedded extension's
iframe wrapper.)The host doesn't register slots in advance — <ExtensionSlot> is the
declaration. If an extension targets a slot ID that no app has placed, it
just won't render anywhere (the install record is harmless).
Each slot publishes whatever shape it wants via the context prop. There's
no schema enforcement in v1 — extensions should null-check fields and fail
gracefully if a field they expect is missing.
Document the context shape next to your <ExtensionSlot> so extension
authors know what to read. Convention: include the document in the slot
ID's prefix section so the agent can find it (mail.contact-sidebar.*
slots all publish { contactEmail, contactName }).
| Action | What it does |
|---|---|
add-extension-slot-target | Mark an extension as installable into a slot (extension author opts in) |
install-extension | Install an extension into a slot for the current user |
uninstall-extension | Remove an extension from a slot for the current user |
list-extensions-for-slot | List installable extensions for a given slot ID |
list-extension-slots | List slot targets an extension declares |
Typical flow when a user asks "add a CRM widget below my contacts":
list-extensions-for-slot { slotId: "mail.contact-sidebar.bottom" } —
see what's already installableinstall-extensioncreate-extension → add-extension-slot-target →
install-extensionMount — host calls the slot installs API, renders an <iframe> per
install. The iframe URL includes ?slot=<slotId> so the runtime knows it's
embedded (enables auto-resize, suppresses anything that only makes sense
full-page).
Context push — host posts agent-native-slot-context immediately on
iframe load, and again on every prop change. The extension reads the
current value synchronously via window.slotContext and subscribes via
window.onSlotContext(fn) for live updates.
Auto-resize — when in slot mode, the iframe runtime measures its
content height and posts agent-native-tool-resize (postMessage type kept
for back-compat) to the host. The <EmbeddedTool> sets the iframe height
accordingly. Use ResizeObserver to follow content changes.
Extension API — embedded extensions have the full helper set:
appAction, appFetch, dbQuery, dbExec, extensionFetch,
extensionData (with toolFetch / toolData legacy aliases). Same auth
context as full-page extensions.
Unmount — uninstall deletes the install row. Polling sync invalidates
the slot-installs query and the host re-renders without the iframe.
<ExtensionSlot>
in any user's view; the slot's contents come from that user's installs./extensions/:id.extensionData. Use actions or app SQL if widgets need
to coordinate.tool_slots table
(Drizzle export extensionSlots), not in the extension's HTML content.
The agent can re-target an extension without rewriting it.connect-builder for the Builder.io Cloud Agent/local
editing handoff in hosted chat, or follow self-modifying-code in a local
code-editing surface.extensions skill — authoring Alpine.js mini-apps (the substrate for widgets)sharing skill — how access flows from extension sharing to slot installscontext-awareness skill — how extensions read what the user is looking atactions skill — how install-extension etc. are auto-mountedd477ebc
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.