The hs-x SDK.
One package, three entry points. defineApp declares the app HubSpot installs, defineWorker declares the code that runs in your Cloudflare account, and defineSource declares the external systems that feed syncs. Everything else in @hs-x/sdk is typed detail around those three: capability methods, result statuses, the handler context, field definitions, flags. This page lists every shipped export with its signature shape and the guide that teaches it.
The 30-second tour
Everything an HS-X app does is declared through this package. The Worker that hs-x init scaffolds is the whole model in one file:
import { defineWorker, ok } from "@hs-x/sdk";
const worker = defineWorker("deals");
worker.tool("tag-high-value-deals", {
label: "Tag high value deals",
objectType: "deal",
input: {
threshold: { type: "number", label: "Amount threshold", default: 50000 },
},
output: {
tagged: { type: "boolean" },
},
async handler({ input, enrolledObject }) {
const amount = Number(enrolledObject.properties.amount ?? 0);
return ok({ tagged: amount >= input.threshold });
},
});
export default worker;defineApp in hsx.config.ts describes the app; Workers under src/workers/ carry the code. The table is the index; the sections below give each export's signature shape, what it declares, and where to learn the journey around it.
| Export | What it declares |
|---|---|
defineApp | The app: name, distribution, auth, scopes, cards, billing, permitted URLs |
card | A UI-extension app card, passed to defineApp |
defineWorker | A named Worker that collects capabilities |
worker.tool / worker.action | A workflow action, optionally exposed to Breeze agents |
worker.cardBackend | A Worker endpoint behind an app card |
worker.trigger | A handler for a HubSpot webhook event |
worker.sync | A scheduled or event-driven data sync |
worker.use / worker.manifest | Register prebuilt capabilities; build the deploy manifest |
defineSource | A pull or push source feeding a sync |
appObject / appObjectAssociation | An app-owned CRM object and its associations |
appEvent | An app-defined CRM event type |
ok / failContinue / failStop / retryLater / block | ActionResult constructors for handler returns |
HandlerContext | What every handler receives |
FieldDefinition / FieldType | Typed input, output, and property fields |
evaluateFlag family | Pure edge feature-flag evaluation |
createHsxOpenFeatureProvider | OpenFeature provider over the same evaluator |
usesScopes | Scope declarations static analysis cannot infer |
redact | Redact secrets, PII, and configured fields before logging |
@hs-x/sdk/ui | The card-iframe surface: logger, dev-mode toggles, createFlagsClient |
@hs-x/sdk/experimental | Declared but gated surface (flag-to-CRM projection) |
What HubSpot installs
defineApp
defineApp(definition: {
name: string;
description?: string;
distribution: 'private' | 'marketplace';
auth: 'static' | 'oauth';
platformVersion: string; // '2026.03'
scopes: string[];
optionalScopes?: string[];
objects?: AppObjectDefinition[]; // from appObject()
objectAssociations?: AppObjectAssociationDefinition[];
events?: AppEventDefinition[]; // from appEvent()
appEvents?: {
batching?: false | { maxSize?: number; maxDelayMs?: number };
};
rateLimits?: {
retry?: false | { max?: number; baseMs?: number; capMs?: number };
bucket?: {
capacity?: number;
refillPerSecond?: number;
searchCapacity?: number;
searchRefillPerSecond?: number;
requestedTokensPerCall?: number;
};
};
cards?: CardDefinition[]; // from card()
billing?: AppBillingDeclaration;
permittedUrls?: { fetch?: string[]; iframe?: string[]; img?: string[] };
}): AppDefinitionThe default export of hsx.config.ts. Codegen turns it into the HubSpot project a portal installs: name and description on the install screen, the distribution model, the auth mode, and the scopes the app requests. App-level declarations attach here too: CRM objects and events, cards, and the billing catalog. hs-x check validates the whole object before anything deploys; the getting started guide tours the file line by line.
appEvents.batching controls runtime auto-batching for ctx.appEvents.send. The default is 500 occurrences or 5000 milliseconds, whichever comes first. Set lower values for latency-sensitive events, or batching: false to bypass auto-batching.
rateLimits controls the managed ctx.hubspot client. Defaults are retry: { max: 5, baseMs: 200, capMs: 10000 }, a general bucket of 100 requests refilling 10 per second, and a Search bucket of 4 requests refilling 4 per second. Override these only when your app's latency budget or approved HubSpot tier needs different behavior.
permittedUrls is the UI-extension allow-list. Cards run sandboxed, and three lists open the sandbox up: fetch for origins the extension may call (your Worker's origin, for card backends), iframe for embeddable pages, and img for image sources. The UI extensions guide shows where each one bites.
card
card(definition: {
id: string;
name: string;
location: 'crm.record.tab' | 'crm.record.sidebar' | 'crm.preview' | 'helpdesk.sidebar';
objectTypes: string[];
entrypoint: string; // path to your React component
description?: string;
}): CardDefinitionDeclares a UI-extension app card; pass the result in defineApp({ cards: [...] }). Codegen emits the HubSpot card metadata, and you author the React entrypoint (hs-x init --ui-extension scaffolds a starter). The card itself is pure UI; anything that needs network or secrets goes through a card backend, covered under Workers below and in depth in the UI extensions guide.
Capabilities live on a Worker
defineWorker
defineWorker(
name: string,
options?: { use?: CapabilityDefinition[] },
): WorkerDefinitionCreates a named Worker, the unit that deploys to your Cloudflare account. The convention is one Worker per file under src/workers/, default-exported. defineWorker takes a string name; a pre-v1 callback form that passed a function no longer exists, so if you find defineWorker((...) => ...) in an old snippet, it predates the shipped API. Capabilities register through the methods below, each of which also returns the definition it created. options.use seeds the Worker with capabilities built elsewhere.
worker.tool and worker.action
worker.tool(id: string, {
label: string;
description?: string;
objectType: string;
input?: FieldMap;
output?: FieldMap;
agent?: { description: string; expose: string[] };
handler(ctx: HandlerContext): ActionResult | OutputShape | Promise<ActionResult | OutputShape>;
}): ToolDefinitionOne declaration, two surfaces. tool and action are the same function (the export action is an alias of tool), and both register a HubSpot workflow action whose input fields become the action's form. Adding the agent block additionally exposes the capability to Breeze agents, with expose naming which input fields the agent may fill. The handler returns an ActionResult or a plain object matching output. Workflow semantics live in the workflow actions guide; agent exposure in the agent tools guide.
worker.cardBackend
worker.cardBackend(id: string, {
label?: string;
objectTypes?: (string | AppObjectDefinition)[];
handler(ctx: HandlerContext): unknown | Promise<unknown>;
}): CardBackendDefinitionThe server half of an app card. The Worker serves it at POST /_hsx/cards/<id>, the extension calls it with hubspot.fetch, and the response envelope is { ok, capabilityId, result } with result carrying whatever the handler returned. This is where card code does network IO, secret-bearing requests, and HubSpot API work through ctx.hubspot. The UI extensions guide wires a full card to its backend.
worker.trigger
worker.trigger(id: string, {
label?: string;
eventType: string; // 'deal.propertyChange.dealstage'
dedup?: 'best-effort' | 'strict';
handler(ctx: HandlerContext): unknown | Promise<unknown>;
}): TriggerDefinitionRuns when HubSpot delivers a webhook event. The runtime owns the endpoint at /webhooks/hubspot/<id>: it verifies HubSpot's v3 signature, drops duplicate delivery ids (a 24-hour window per id, when a dedup store is configured), queues bursts, and invokes the handler once per event. The two dedup modes share that store check and differ on the race window around it: 'strict' claims the delivery id synchronously before reading the store, so a concurrent duplicate cannot slip between the read and the write, while 'best-effort' skips the claim and accepts that small window for lower contention. eventType follows HubSpot's subscription naming. The triggers guide covers signatures, dedup trade-offs, and queueing.
worker.sync
worker.sync(id: string, {
label?: string;
schedule: string; // interval ('5m'), cron expression, or 'event'
into?: string; // destination object, e.g. 'p_customer'
schema?: Record<string, unknown>;
manageSchema?: false | 'properties' | 'full';
handler(ctx: SyncHandlerContext): unknown | Promise<unknown>;
}): SyncDefinition
worker.sync(source: SourceDefinition, {
// same options; id defaults to the source name and handler is optional
}): SyncDefinitionMoves data into a destination object. The second call shape takes a source from defineSource: id defaults to the source name, the handler becomes optional (omitted, it defaults to one that just reports success), and the source definition rides into the Worker's manifest. The row pipeline that would ingest source rows automatically — batched writes into the destination object, paced through the portal's shared rate budget — is not live yet, so a sync that actually moves data still does the work in its handler. schedule is likewise recorded in the manifest; nothing fires it on a clock yet, so runs are driven explicitly through the Worker's /sync/<id>/run endpoint (hs-x dev invoke locally). manageSchema decides how much portal schema HS-X owns: 'full' manages the object and its properties, 'properties' manages properties on an existing object, false touches nothing. Sync handlers get ctx.sync for cursor reads and writes. The whole journey is the syncs guide.
worker.use and worker.manifest
worker.use(...capabilities: CapabilityDefinition[]): WorkerDefinition
worker.manifest(): WorkerManifestuse registers capabilities built elsewhere, which is how a Worker splits across files: the package also exports standalone tool, action, cardBackend, trigger, and sync helpers that build the same definitions without registering them. manifest is what build and deploy call to produce the Worker's capability manifest; it throws on duplicate ids, and since tool and action share one namespace, a tool and an action with the same id collide.
usesScopes
usesScopes<T>(scopes: readonly string[], run: () => T): ScopeUsage<T>A declaration marker: wrapping a call records the HubSpot scopes it needs, for analysis that cannot see inside a raw fetch. The analysis is narrower than the marker today: hs-x check warns when it spots a raw fetch against a HubSpot API (pointing you at ctx.hubspot or this wrapper) and checks card scopes against the declared list, but it does not yet read usesScopes declarations or infer scopes from ctx.hubspot calls. Declaring them now means your code is already accurate when that inference lands.
Where sync rows come from
defineSource and defineSource.push
// Pull: fetch pages on a schedule.
defineSource({
name: string;
auth?: { type: 'bearer' | 'oauth2' | 'basic' | 'hmac'; /* credential fields */ };
fetch(ctx: { cursor?: TCursor; http: SourceHttpClient }):
{ rows: { key: string; data: TData }[]; cursor?: TCursor };
}): PullSourceDefinition
// Push: rows arrive on a webhook.
defineSource.push({
name: string;
auth?: { type: 'bearer' | 'oauth2' | 'basic' | 'hmac'; /* credential fields */ };
receive(ctx: { event: TEvent }):
{ rows: { key: string; data: TData }[] };
}): PushSourceDefinitionA source describes an external system in two verbs. A pull source implements fetch: it receives the cursor persisted from the previous run and returns a page of { key, data } rows plus the cursor for the next one; a page with no cursor ends the run. A push source implements receive, turning each delivered event into rows; the manifest reserves a webhook path for it at /webhooks/<name>. Know where the line sits today: a source is declared contract riding in the manifest, and the runtime pipeline that would drive it — paging fetch, serving the /webhooks/<name> endpoint, verifying the declared auth before receive runs — is not live yet. Either kind attaches to a worker with worker.sync(source, {...}). Pull and push are walked end to end in the syncs guide.
What a handler returns
ok, failContinue, failStop, retryLater, block
type ActionResult =
| { status: 'ok'; output?: Record<string, unknown> }
| { status: 'fail-continue'; message: string; output?: Record<string, unknown> }
| { status: 'fail-stop'; message: string }
| { status: 'retry-later'; message: string; retryAfterSeconds?: number }
| { status: 'block'; message: string };
ok(output?) // success, optionally with output fields
failContinue(message, output?) // record the failure, let the workflow advance
failStop(message) // halt enrollment for this record
retryLater(message, retryAfterSeconds?) // transient failure, with a retry-delay hint
block(message) // policy stop, distinct from a failureFive constructors, one for each ActionResult status, and the runtime maps each onto HubSpot's execution contract on the inline action path: ok returns the output fields with a 200; fail-continue returns them with hs_execution_state: FAIL_CONTINUE, so the action is recorded as failed and the workflow advances; block returns hs_execution_state: BLOCK, pausing the enrollment until the block expires (HubSpot's default is one week); fail-stop answers 400, a failure HubSpot does not retry; and retry-later answers 429 with retryAfterSeconds as a Retry-After header (or 503 without a hint), which HubSpot requeues with exponential backoff for up to three days. The full result also lands in the run checkpoint. An uncaught throw still surfaces as a runtime error rather than a tidy failure result, so return explicit results either way. The workflow actions guide maps each status to what HubSpot shows.
What every handler receives
HandlerContext
interface HandlerContext<TInput, TEnv> {
input: TInput; // typed from the capability's input fields
enrolledObject: { // the CRM record the capability runs against
id: string;
objectType: string;
properties: Record<string, unknown>;
};
install: InstallContext; // id, portalId, state, config()
env: TEnv; // Worker environment bindings
hubspot: HubSpotClient; // authenticated, rate-limited HubSpot client
appObjects: AppObjectsContext; // get / create / update / archive
appEvents: AppEventsContext; // send / sendBatch
http: HttpClient; // plain outbound HTTP
billing?: BillingContext; // recordUsage / recordCredit / current
sync?: SyncCursorContext; // cursor() / setCursor(), sync handlers only
flags?: FlagsContext; // getBoolean / getString / getNumber / getJson
logger: Logger; // debug / info / warn / error, structured fields
request: Request; // the raw incoming Request
}Every capability handler gets the same context, so code moves between capability kinds without rewiring. input is typed from the declared field map. enrolledObject is the record the capability was invoked against. install identifies the installing portal and exposes config() for per-install configuration. hubspot is the authenticated client scoped to that install, already paced against the portal's rate budget; http is for everything that is not HubSpot.
Three members are optional because they depend on what the project declares. billing exists on platform-linked deploys whose app declares a billing catalog, and is undefined otherwise; the billing guide covers both modes. sync is the cursor store, present in sync handlers. flags reads feature flags from the KV snapshot in your own account, each getter failing safe to the default you pass; the feature flags guide explains the evaluation model. To poke at any of this locally, hs-x dev invoke runs a capability through the production router with fixture context, per the local dev guide.
Objects and events the app owns
appObject
appObject(id: string, {
name: string;
label: string;
singularForm: string;
pluralForm: string;
primaryDisplayLabelPropertyName: string;
properties: Record<string, AppObjectPropertyDefinition>;
// plus optional: uid, description, appPrefix, settings, requiredProperties,
// searchableProperties, propertyGroups, defaultCreateFormFields,
// secondaryDisplayLabelPropertyNames
}): AppObjectDefinitionDeclares an app object: a CRM object type your app owns, created in the portal at install time. Pass the result in defineApp({ objects: [...] }). Each property is a field definition with object-specific extras like hasUniqueValue and readOnlyValue. Handlers then work with typed records through ctx.appObjects.get, create, update, and archive, where the property types flow from the declaration. All three declarations in this section take an optional uid, the portal-side identity of the type: omit it and the SDK derives one from the id as UPPER_SNAKE (flag-rollout becomes FLAG_ROLLOUT). HubSpot keys the installed type by uid, so pick it once and keep it stable. The app objects guide builds one from scratch.
appObjectAssociation
appObjectAssociation(id: string, {
fromObjectType: string;
toObjectType: string;
name?: string;
label?: string;
inverseLabel?: string;
cardinality?: 'ONE_TO_ONE' | 'ONE_TO_MANY' | 'MANY_TO_ONE' | 'MANY_TO_MANY';
uid?: string;
}): AppObjectAssociationDefinitionDeclares an association between an app object and another object type, with an optional internal name, labels for both directions, and a cardinality. Registered in defineApp({ objectAssociations: [...] }); uid follows the same derive-from-id rule as appObject.
appEvent
appEvent(id: string, {
name: string;
label: string;
objectType: string; // which CRM object the event attaches to
properties: Record<string, AppEventPropertyDefinition>;
// plus optional: uid, description, supportsCustomObject, headerTemplate, detailTemplate
}): AppEventDefinitionDeclares an app event type that handlers emit onto CRM records with ctx.appEvents.send(event, occurrence) or sendBatch. send auto-batches by install at the app's configured appEvents.batching size and delay. An occurrence targets a record by objectId (or email/utk for contacts) and carries typed properties from the declaration. The app events guide covers templates, timelines, and triggering on your own events.
Typed inputs, outputs, and properties
FieldDefinition and FieldType
type FieldType = 'string' | 'number' | 'bool' | 'boolean' | 'enumeration' | 'datetime' | 'json';
interface FieldDefinition {
type: FieldType;
label?: string;
description?: string;
required?: boolean;
default?: unknown;
options?: readonly string[] | { value: string; label: string }[];
fieldType?: string; // HubSpot UI control, e.g. 'select'
valueSource?: 'static' | 'property' | 'both';
supportedValueTypes?: ('STATIC_VALUE' | 'OBJECT_PROPERTY' | 'FIELD_DATA')[];
isRequired?: boolean; // HubSpot's spelling of required
}
interface OutputFieldDefinition {
type: FieldType;
label?: string;
description?: string;
fieldType?: string;
}One field shape serves capability input/output maps, app-object properties, and app-event properties. The choice list type is 'enumeration' and its choices live in options, as plain strings or { value, label } pairs; there is no 'enum' type and no values key. Legacy uppercase HubSpot spellings ('STRING', 'ENUMERATION', and friends) are accepted and normalized at codegen time.
The last three members exist for the workflow-action form. supportedValueTypes tells HubSpot how the field may be filled — typed in statically, mapped from an object property, or wired from another field's data — and codegen defaults it to static-only; valueSource declares the same intent in SDK terms. isRequired is HubSpot's spelling of required: codegen reads either, and a field with neither is required. Output maps need none of that, so OutputFieldDefinition is the narrower shape — type, label, description, control.
Field maps drive typing end to end: InferInput turns a declared map into the handler's input type, so a number field arrives as number and an enumeration with literal options narrows to a union of those strings. Two helpers back the normalization and are exported for tooling: normalizeHubSpotFieldType maps any accepted spelling to its canonical lowercase token (via the HUBSPOT_FIELD_TYPE_MAP table), and defaultHubSpotFieldType picks the HubSpot UI control a type gets when fieldType is omitted.
Feature flags at the edge
evaluateFlag and friends
evaluateFlag(snapshot, context, defaultValue): FlagEvaluationResult
evaluateBooleanFlag(snapshot, context, defaultValue): FlagEvaluationResult<boolean>
evaluateStringFlag(snapshot, context, defaultValue): FlagEvaluationResult<string>
evaluateNumberFlag(snapshot, context, defaultValue): FlagEvaluationResult<number>
evaluateJsonFlag<T>(snapshot, context, defaultValue): FlagEvaluationResult<T>
evaluateFlags(
snapshots: ReadonlyMap<string, FlagSnapshot | undefined>,
context,
defaults: ReadonlyMap<string, unknown>,
): Record<string, FlagEvaluationResult>The pure flag evaluator: no IO, no control-plane call. The first argument is the FlagSnapshot from KV — the compiled flag, not the authoring-side definition — and the default is required because every failure mode resolves to it: a missing snapshot, a dangling variation reference, a type-mismatched typed getter. The return is never a bare value; read .value off the FlagEvaluationResult, which also carries the winning variation and a reason. evaluateFlags batches one context across many snapshots, keyed by the defaults map. This is the same evaluator ctx.flags runs against the KV snapshot in your Worker, exported so tests and tooling can evaluate flags without a runtime.
createHsxOpenFeatureProvider
createHsxOpenFeatureProvider(options: HsxOpenFeatureProviderOptions): HsxOpenFeatureProviderAn OpenFeature-compatible provider over the same evaluator, for code already standardized on the OpenFeature client API. Resolution reasons and error codes follow the OpenFeature spec. Authoring flags, targeting rules, and the kill switch are the feature flags guide.
Redaction
redact
import { redact } from '@hs-x/sdk';
logger.info('verification result', redact({
email: 'mia@example.com',
note: 'customer name',
status: 'deliverable',
}, { fields: ['note'] }));The helper mirrors the runtime's logger redactor: token-like keys, bearer/PAT/HS-X token strings, email addresses, and phone-shaped strings become [redacted]; fields force-redacts app-specific keys the generic rules cannot infer. Add redaction: { fields: [...] } to defineApp(...) when the runtime should apply the same forced field list to every handler logger payload.
Inside the card iframe
Card code imports from @hs-x/sdk/ui, never from @hs-x/sdk: the main barrel is backend-only, and ui is the one entry point built to run inside a HubSpot UI-extension iframe. The surface is small by design — a logger, an explicit dev-mode override for it, and a feature-flag client for cards that have no backend. @hubspot/ui-extensions is an optional peer dependency that the subpath imports dynamically, so a Node consumer pulling it for types alone resolves cleanly without the peer installed.
logger
import { logger } from '@hs-x/sdk/ui';
logger.info('rendered'); // debug / info / warn / error
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
interface Logger {
debug(message: string): void;
info(message: string): void;
warn(message: string): void;
error(message: string): void;
}The API mirrors the @hubspot/ui-extensions logger exactly: four levels, one string argument. Each call does two things. It always delegates to HubSpot's logger when the peer is present, so production logs batch and ship to HubSpot's log viewer with trace ids and rate limits handled on HubSpot's side. In local dev it additionally forwards the call to the hs-x dev log sidecar, which is how card logs land in your terminal next to the backend's.
Dev detection keys off the module's own origin. HubSpot's local dev server serves card bundles from localhost and production bundles come from HubSpot's CDN, so a bundle loaded from a localhost origin forwards and a CDN bundle does not. Forwarding mirrors HubSpot's own batching shape — up to 100 entries per POST on a roughly 5-second cadence, flushed with sendBeacon when the page hides so the last batch survives closing the card. Every send is fire-and-forget; a logging call never throws, whatever the network does.
enableHsxDev, disableHsxDev, DEFAULT_HSX_DEV_LOG_URL
enableHsxDev(url?: string): void // url defaults to DEFAULT_HSX_DEV_LOG_URL
disableHsxDev(): void
const DEFAULT_HSX_DEV_LOG_URL = 'http://127.0.0.1:9099/__hsx/log';The explicit override for when auto-detection reads the situation wrong in either direction: a bundler that rewrites import.meta.url hides dev mode, and a test build served from localhost fakes it. enableHsxDev() switches forwarding on, optionally at a non-default sidecar URL; disableHsxDev() switches it off, auto-detection included. The sidecar is its own listener inside the hs-x dev process — port 9099 by default (--hsx-log-port), separate from the dev server's HTTP API — and it is loopback-only by design. enableHsxDev throws on any URL whose host is not localhost, 127.0.0.1, or ::1, and the logger re-validates the URL on every call, so free-form log messages (which may carry record data or secrets) cannot be pointed off-host.
createFlagsClient
createFlagsClient(options: {
endpoint: string; // tenant Worker origin; must be in permittedUrls.fetch
targetingContext?: Record<string, unknown>; // overlay applied to every evaluation
timeoutMs?: number; // forwarded to hubspot.fetch (its own default: 15s)
fetchImpl?: HubSpotFetchLike; // transport override, primarily a test seam
}): FlagsClient
interface FlagsClient {
getBoolean(key: string, defaultValue: boolean): Promise<boolean>;
getString(key: string, defaultValue: string): Promise<string>;
getNumber(key: string, defaultValue: number): Promise<number>;
getJson<T>(key: string, defaultValue: T): Promise<T>;
evaluate(
keys: readonly string[],
targetingContext?: Record<string, unknown>,
): Promise<Record<string, FlagClientResult>>;
}
interface FlagClientResult { value: unknown; variation?: string; reason: string }Feature flags for pure-UI cards. A card with a backend should read flags in the backend handler through ctx.flags; a card without one resolves them over the wire, against the same Worker the project deploys:
POST <endpoint>/_hsx/flags/evaluate
{ "flagKeys": ["new-card"], "targetingContext": { "portalId": "555000" } }
200 { "ok": true, "flags": { "new-card": { "value": true, "variation": "on", "reason": "targeting_match" } } }The endpoint reads the KV snapshot in your account and runs the same pure evaluator behind ctx.flags; neither the control plane nor HubSpot is in the path. The Worker's own configuration supplies the identity dimensions (account, project, environment, app id), the client's targetingContext overlays per-user targeting on top, and evaluate's second argument overlays again per call. reason is the standard evaluation reason: targeting_match, rollout, default, disabled, flag_not_found, or error.
Inside a real card iframe the transport is hubspot.fetch, the sandbox's only sanctioned egress — which is why the Worker origin must be listed in permittedUrls.fetch, and why the request body travels as a plain object with no custom headers (hubspot.fetch's calling convention; HubSpotFetchLike is its minimal contract, and a custom fetchImpl matches it). Outside the iframe, in tests and Node scripts, the client adapts the same call onto native fetch.
Every getter fails safe to the default you pass. A missing transport, a non-200, a malformed body, a thrown fetch, a key the Worker doesn't know, and a boolean, string, or number value that doesn't match its typed getter all resolve to the default; getJson checks only that the value is non-null before casting. A flag read can never break a render. There is no client-side cache, so each getter call is a full round trip, and a card that reads several flags should make one evaluate([...keys]) call and pull values from the returned map. The UI extensions guide wires a card end to end, the feature flags guide covers authoring and targeting, and the local dev guide shows the sidecar loop.
Declared but gated
@hs-x/sdk/experimental
import {
featureFlagAppObject,
flagDefinitionToAppObjectRecord,
flagToCompanyAssociation,
flagToContactAssociation,
} from '@hs-x/sdk/experimental';The experimental subpath holds surface that is declared but not yet delivered end to end: today, the feature-flag CRM projection. featureFlagAppObject declares the flag app object (with FEATURE_FLAG_APP_OBJECT_TYPE and FEATURE_FLAG_APP_OBJECT_SCOPES constants), flagDefinitionToAppObjectRecord maps a flag definition to a record, the two association helpers link flags to companies and contacts, and flagDestinationSourceId / flagDestinationMatchKey derive sync identities. All of it is pure functions, but the push-to-CRM delivery path is gated in v1, so treat these as experimental: they can change or graduate to the main barrel without a major version.
Type re-exports
Back on the main barrel, @hs-x/sdk also re-exports the types its functions produce and consume, so your own helpers can be typed without reaching into internals: AppDefinition, WorkerManifest, CapabilityManifest, CardDefinition, SourceManifest, the billing declaration family (AppBillingDeclaration, ProjectBillingPlan, BillingPricing, and the rest), and the HubSpotClient / HttpClient types behind ctx.hubspot and ctx.http. SDK_VERSION is the package version as a constant.
Run hs-x check first: it validates every declaration on this page (ids, field types, schemas, scopes) and reports findings with file and line before anything deploys. For handler behavior, hs-x dev invoke <capability-id> runs the capability through the production router locally.
Where the exports lead
- Getting started —
defineApp, the scaffold, and the first deploy, narrated. - Workflow actions —
worker.tool, result statuses, and the 20-second budget. - Agent tools explains the
agentblock and Breeze exposure. - Triggers —
worker.trigger, signatures, dedup, and queueing. - Syncs —
defineSource,worker.sync, cursors, and schema management. - UI extensions —
card, card backends, andpermittedUrls. - App objects and app events — the CRM types your app owns.
- Feature flags covers
ctx.flagsand the edge evaluation model. - Billing pairs the billing declaration with
ctx.billing. - The CLI reference lists the commands that validate, run, and deploy what you declare here.
Last updated: July 24, 2026. Reflects @hs-x/sdk v0.3.0 (SDK_VERSION); shapes mirror the package's typed barrel. Refreshed whenever the surface changes.