This document is maintained with the Channel App SDK. Check the SDK README first for current packages and the complete reading order. The source of truth for this Document is the GitHub source.
An Extension is a named, versioned contract that connects typed app Functions to a standard Channel capability. Channel surfaces know how to discover and invoke a command, widget, custom tab, hook, OAuth flow, or other capability because the app implements that Extension's official Function names and schemas.
An Extension normally contains two kinds of Functions:
Function kind | Purpose | Example |
|---|---|---|
Metadata or discovery | Describes the capability and points to runtime Functions |
|
Runtime or action | Performs the user-visible or background operation |
|
Metadata may reference a standalone app Function such as orders.sync. Keep app-specific business
operations standalone, and use the Extension namespace only for the standard contract. Inside an
Extension, a relative name becomes extension.{extensionName}.{relativeName}.
Registration does not upload or deploy app code. It announces the app-level
(extensionName, systemVersion) contract so AppStore can call the configured Function Endpoint and
discover schemas. It also does not install or enable the app in an individual Channel; installation,
permission grants, and capability activation are separate steps.
Use the SDK-owned Extension family, Function names, and schemas. A metadata response that points to an action Function does not create that Function automatically; both the metadata Function and every referenced runtime Function must be registered in the app server.
Use @Extension for the family and system version, @Func for relative names, and register the
decorated class as a NestJS provider. ChannelAppModule discovers the provider and performs the
recommended registration flow after the HTTP listener is ready.
If the class is missing from providers, discovery cannot see it. If autoRegister is false, the
SDK still dispatches implemented Functions, but it does not publish their Extension target to
AppStore.
Use the typed extension/{family} builder with app.Use. The builder declares both the Function
schemas and the Extension target. server.WithAutoRegister() starts registration after the server
can answer discovery requests.
Use server.WithAutoRegisterRetry and server.WithAutoRegisterResult when deployment policy needs
custom retry or observability. Existing Gin servers use the equivalent options from server/gin.
The recommended auto-registration flow:
waits until the Function server is listening;
gets one cached app token through
TokenManager;calls
registerExtensionfor every discovered Extension name and system version;retries transient failures with bounded exponential backoff;
lets AppStore call the versioned Function Endpoint for schema and metadata discovery.
The request fields are camelCase: appId, extensionName, and systemVersion. systemVersion such
as v1 is the Channel Extension contract version, not the release version of your app.
Only custom bootstraps or deployment-controlled registration normally call the native Function directly:
SDK | Explicit call |
|---|---|
TypeScript |
|
Go |
|
Do not issue a new token or register on every Function request. Apps with only standalone Functions
use the SDK's core:v1 fallback. Advanced families such as ALF task, Notebook, and Messaging may
require a secondary sync or coordinated product setup after generic Extension registration; follow
their family recipe.
Deploy the Function Endpoint before registration because AppStore may call discovery immediately.
Keep auto-registration enabled on normal startup and use its bounded retry instead of a custom infinite loop.
In a multi-replica deployment, use shared token storage. Duplicate idempotent registration calls are acceptable, but every replica must not issue its own uncached token loop.
Re-register after changing Extension schemas, metadata, Function names, permissions, or the Function Endpoint. Do not change
systemVersionmerely for an app release.Use
unregisterExtensiononly when intentionally removing a capability; deployment rollback alone should restore the last compatible server and schemas.
Verify these boundaries separately:
startup logs show the expected Extension name, system version, and successful registration;
getFunctionsdiscovery contains every metadata and referenced runtime Function;metadata appears on the intended Channel surface after installation and activation;
one real runtime call succeeds in a test Channel;
invalid input, invalid signature, missing permission, and transient registration failure are rejected or retried as designed.
A successful registerExtension response proves only that the registration request was accepted.
It does not prove that discovery, metadata validation, Channel installation, activation, or runtime
handlers work.
For every Extension:
enable only the permissions used by its Functions;
implement metadata and referenced Functions with SDK schemas;
use SDK auto-registration with an app token;
test discovery, valid calls, invalid input, missing authorization, and retries;
keep App Secret, Signing Key, app/channel tokens, and provider credentials out of WAM code.
TypeScript apps normally use @Extension and @Func. Go apps should prefer the typed
extension/{family} builder. Each family recipe below covers both languages, authentication, WAM,
reliability, and testing, then links the exact TypeScript schemas and
Go Extension reference.
Read Function registration for the shared wire contract and apply the WAM guide to any Extension that opens UI.
Use config for API keys, client_credentials, shop identifiers, and other scoped settings.
Implement extension.config.metadata.getConfigSchema. Optional validation, save, and delete
Functions may enforce provider rules. Mark secrets as credentials, localize labels rather than
stable keys, and read injected values from Function context instead of sending them to a WAM.
Use oauth only for a provider's Authorization Code flow. Implement
extension.oauth.metadata.getAuthConfig and register oauth:v1. AppStore owns redirect state and
injects the connected provider token as ctx.authToken. Do not use this Extension for API keys or
client_credentials; those belong in Config.
extension.command.metadata.getCommands publishes Desk commands. Each command must reference the
exact full name of a standalone or Extension Function. Use a command to return text, perform an
action, or open a WAM. Test command discovery separately from the referenced action handler.
Command recipe · WAM guide · TypeScript tutorial · Go tutorial
extension.widget.metadata.getWidgets publishes contextual widgets. Widget metadata selects the
surface and action Function; the action can return a WAM. Treat chat, user, and manager fields as
surface-dependent optional context and verify permissions for every native action.
extension.customtab.metadata.getCustomTabs publishes app-owned tabs. Keep tab identifiers stable,
point actions to exact Function names, and use a WAM for interactive content. Do not place tokens or
private records in tab metadata or wamArgs.
extension.hook.metadata.getHooks declares event-driven Functions. Make handlers idempotent,
authenticate signed app Function calls, and return quickly when the event can be processed
asynchronously. Public webhook.received targets require a public targetId, a high-entropy
endpointToken, payload validation, replay protection, and secret rotation.
extension.polling.metadata.getPollers declares scheduled pollers. A target resolver such as
target.getChannels pages through installed channels, and each poller names a full Function to
invoke. Store cursors durably, make retries idempotent, bound each batch, and test partial failure.
Use calendar for calendars, event types, availability, booking creation, cancellation, and
queries. Keep provider credentials server-side, normalize time zones explicitly, and make booking
mutations idempotent. A WAM is appropriate for slot selection while server Functions own provider
calls.
extension.store.metadata.getStoreProfile publishes store identity and presentation metadata.
AppStore reads the profile during registration or synchronization. Keep stable IDs separate from
localized labels and do not include provider credentials in the profile.
DataSource metadata exposes catalogs, tables, columns, and table descriptions. Query execution uses
the authenticated DataSource gRPC endpoint rather than the normal app Function route. Validate
x-access-token, enforce catalog/table allowlists, parameterize SQL, cap rows and time, and stream
Arrow-compatible results. The SDK includes PostgreSQL and BigQuery-oriented runners.
DataSource recipe · Go examples
Use the redesigned commerce Extension for new commerce apps. It provides the ID-based order model,
buyer information, order lookup, cancel/return/exchange requests, exchangeable items, shipping
address changes, and structured ActionResult responses. Validate provider state before mutations
and return explicit unsupported results when a provider lacks an operation.
wms connects warehouse/order-management providers. Use the ID-based
extension.wms.order.* Functions for order lookup, cancel/return/exchange restore flows, and
shipping-address changes. Require explicit shop configuration and test reversible mutations in a
safe environment.
Messaging covers inbox, prebuilt messaging, follow-up, medium-link, and CHX integrations. It is more AppStore-driven than other families and still uses generic registration plus several channel-scoped native Functions. Design the required native claims first, persist external conversation/message mappings, make webhook or polling delivery idempotent, and never impersonate a user without the proper user/manager authorization.
extension.alfTask.alftask.getTasks publishes versioned automation tasks. Registration has two
steps: registerExtension("alfTask", "v1") and registerAlfTasks. Keep task keys stable, increment
versions for behavior changes, and verify the synchronized versions.
extension.notebook.core.getNotebooks publishes versioned notebook definitions. Registration also
requires registerAppNotebooks. Keep notebook and cell keys stable, increment versions for
definition changes, and treat rendered content as untrusted when it includes external data.
mailRelay receives normalized mail events through
extension.mailRelay.inbound.onMailReceived. TypeScript 0.17.2 registers that full Function name
as a standalone @Func and calls registerExtension("mailRelay", "v1") explicitly; Go provides a
typed builder. Validate relay tokens, bound attachments and body size, deduplicate message IDs, and
avoid logging raw mail content.
Metadata uses the exact SDK schema and full Function names.
The Extension class/provider or Go builder is registered once.
Function requests reject missing or invalid signatures.
App/channel tokens are cached and refreshed; manager/user authorization remains in the WAM host.
Provider credentials are injected from Config/OAuth and never returned to the client.
Mutations are idempotent or safely retryable and have explicit permission-failure behavior.
Discovery and at least one real invocation pass in an installed test app.
After implementation passes, use the production readiness guide as the final security, reliability, deployment, operations, and rollback gate.