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. A successful registerExtension call automatically synchronizes ALF tasks and Notebooks; do not call a separate registration Function for either family. Messaging and other advanced families may still require coordinated product setup or a family-specific secondary sync, so follow their family recipe.
systemVersion is the system contract version between AppStore and the app server. Channel increments it when fields, schemas, or invocation behavior in requests that AppStore sends to third-party apps introduce a backward-incompatible breaking change. It is not an app deployment version, an npm or Go SDK package version, or a developer-defined Function API version. The current default system contract is v1. For example, an AppStore system contract v2 would identify a platform contract with those breaking changes; it would not be a v2 created by each app.
Where | Meaning |
|---|---|
| The system contract that the app declares it implements to AppStore |
An Extension metadata target | The same AppStore system contract to use when invoking that Function |
Request | Request and route representations of the same contract sent by AppStore |
The current SDK does not create a separate handler set for each version. Developers therefore cannot operate their own Function v2 by setting systemVersion: "v2". Use that value only after AppStore and the SDK publish an official v2 contract. See Function registration for preserving compatibility in app-owned standalone Functions. Keep the official names of standard Extension Functions and implement the contract published by AppStore and the SDK.
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. App scope uses a high-entropy endpointToken; manager scope receives an AppStore-issued URL in Function context. Both require provider payload validation and replay protection.
extension.polling.metadata.getPollers declares scheduled pollers. Scope-specific resolvers page through channel targets (target.getChannels) or channel/manager targets (target.getManagers), 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, let AppStore authorize exposed tables, enforce read-only SQL with app-scoped credentials, 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. Registering through registerExtension("alfTask", "v1") also triggers task synchronization. Keep task keys stable, increment versions for behavior changes, and verify the synchronized versions.
extension.notebook.core.getNotebooks publishes versioned notebook definitions. Registering the Notebook extension through registerExtension also triggers synchronization. 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.
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.