Cookbook
Capability Examples
Explore working examples of each SDK capability integrated into Surface components. Every example is self-contained and ready to use — copy it into your extension and add the required permission to your manifest.json.
context.read — Reading Platform Context
Read customer and session data provided by the framework. The useContextData hook handles loading state automatically.
Permission: context:read
import { useContextData, Surface, ui } from '@stackable-labs/sdk-extension-react'
export function Header(): React.ReactElement {
const { loading, customerId, customerEmail } = useContextData()
if (loading) {
return (
<Surface id="slot.header">
<ui.Text className="text-xs text-muted-foreground">Loading...</ui.Text>
</Surface>
)
}
return (
<Surface id="slot.header">
<ui.Stack direction="column" gap="1">
<ui.Text className="text-xs font-semibold">{customerEmail}</ui.Text>
<ui.Text className="text-xs text-muted-foreground">ID: {customerId}</ui.Text>
</ui.Stack>
</Surface>
)
}
data.query — Platform-Mediated Requests
Send structured requests to the platform. The framework handles the API call and returns the result — no allowedDomains needed.
Permission: data:query
import { useCapabilities, Surface, ui } from '@stackable-labs/sdk-extension-react'
import { useState } from 'react'
import type { ApiRequest } from '@stackable-labs/sdk-extension-contracts'
export function Content(): React.ReactElement {
const capabilities = useCapabilities()
const [data, setData] = useState<unknown>(null)
const handleQuery = async () => {
const payload: ApiRequest = {
action: 'getCustomer',
customerId: '123',
}
const result = await capabilities.data.query<{ name: string }>(payload)
setData(result)
}
return (
<Surface id="slot.content">
<ui.Button onClick={handleQuery}>Fetch Data</ui.Button>
</Surface>
)
}
data.fetch — Direct HTTP Requests
Make HTTP requests directly from the extension sandbox. The domain must be listed in allowedDomains in your manifest — exact hostnames or *.<suffix> wildcards. See External APIs > Wildcards for the full rules.
Permission: data:fetch
import { useCapabilities, Surface, ui } from '@stackable-labs/sdk-extension-react'
import { useState } from 'react'
import type { FetchResponse } from '@stackable-labs/sdk-extension-contracts'
export function Content(): React.ReactElement {
const capabilities = useCapabilities()
const [data, setData] = useState<unknown>(null)
const handleGet = async () => {
const result: FetchResponse = await capabilities.data.fetch('https://api.myservice.com/orders')
if (result.ok) setData(result.data)
}
const handlePost = async () => {
const result: FetchResponse = await capabilities.data.fetch(
'https://api.myservice.com/orders',
{ method: 'POST', body: { limit: 10 } }
)
if (result.ok) setData(result.data)
}
return (
<Surface id="slot.content">
<ui.Stack gap="2">
<ui.Button onClick={handleGet}>GET Orders</ui.Button>
<ui.Button onClick={handlePost}>POST Orders</ui.Button>
</ui.Stack>
</Surface>
)
}
actions.toast — Toast Notifications
Display toast notifications in the host widget's UI to provide feedback to users.
Permission: actions:toast
import { useCapabilities, Surface, ui } from '@stackable-labs/sdk-extension-react'
export function Content(): React.ReactElement {
const capabilities = useCapabilities()
const showToast = async () => {
await capabilities.actions.toast({ type: 'success', message: 'Done!' })
}
return (
<Surface id="slot.content">
<ui.Button onClick={showToast}>Show Toast</ui.Button>
</Surface>
)
}
actions.invoke — Host Actions
Trigger host-defined actions like starting conversations, setting tags, or updating custom fields.
Permission: actions:invoke
import { useCapabilities, Surface, ui } from '@stackable-labs/sdk-extension-react'
export function Content(): React.ReactElement {
const capabilities = useCapabilities()
const newConversation = async () => {
await capabilities.actions.invoke('newConversation', {
tags: ['stackable', 'order-lookup'],
fields: [{ id: 'stackable_action', value: 'order_status' }],
metadata: { orderId: '12345' },
})
}
const setTags = async () => {
await capabilities.actions.invoke('setConversationTags', ['escalated', 'order-issue'])
}
return (
<Surface id="slot.content">
<ui.Stack gap="2">
<ui.Button onClick={newConversation}>New Conversation</ui.Button>
<ui.Button onClick={setTags}>Set Tags</ui.Button>
</ui.Stack>
</Surface>
)
}
messaging.send — Sending Messages to Conversations
Post a message into the active conversation bound to this Instance via the useMessaging React hook. Returns the response on happy path and throws on actionable errors — inspect error for one of the typed SendMessageActionableErrorCode values (invalid_message, rate_limited, upstream_error). Host-handled cases (no_conversation, reauth_required, forbidden) resolve to null without throwing; the SDK logs a breadcrumb and the host surfaces remediation to admins (e.g. dashboard reconnect UI) — extensions don't catch them.
Permission: messaging:send. The author label rendered above outbound messages is set per-Instance by the admin via the Instance settings field messagingDisplayName (visible on the dashboard once OAuth completes); falls back to extension.manifest.name when blank. Extensions do not set or think about the author identity.
import { useMessaging, Surface, ui } from '@stackable-labs/sdk-extension-react'
// Requires 'messaging:send' permission. send() throws on actionable errors
// (state.error holds the typed code); resolves to null on host-handled cases.
export function Content(): React.ReactElement {
const [send, { enabled, loading, error }] = useMessaging()
const onSayHello = async () => {
try {
await send({
kind: 'text',
body: 'Hello from the extension',
actions: [
{ type: 'reply', label: 'Sounds good', payload: 'ACK' },
{ type: 'reply', label: 'Maybe later', payload: 'DEFER' },
],
})
} catch {
// error holds the typed SendMessageActionableErrorCode
}
}
return (
<Surface id="slot.content">
<ui.Stack direction="column" gap="2" className="p-3">
<ui.Button onClick={onSayHello} disabled={!enabled || loading}>Say hello</ui.Button>
{(error === 'rate_limited') && <ui.Alert variant="warning">Slow down — sent too many.</ui.Alert>}
{(error === 'upstream_error') && <ui.Alert variant="error">Send failed — please try again.</ui.Alert>}
{(error === 'invalid_message') && <ui.Alert variant="error">Send failed — message format invalid.</ui.Alert>}
</ui.Stack>
</Surface>
)
}