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

Auto-generated from Stackable Extension SDK. Questions/Issues? developers@stackablelabs.com

Previous
Structural Patterns
Capability Examples | Stackable Labs :. Dev Documentation