Skip to main content
Actions are deterministic server handlers a widget calls directly: a user clicks Save, the handler runs with the same ctx every other handler gets, and the widget handles the result. There is no assistant and no natural-language step, so an action has no parameters and no proposes classification, unlike a tool. For the local dev loop (fixtures mode, live mode, the live-mode confirmation) see Harness actions.

Declare an action

Actions live in a record on defineIntegration(), keyed by a kebab-case name. input (and the optional output) are real zod schema instances, not JSON-schema stand-ins.
Action names are validated at import time. A name that is not kebab-case fails with Action names are kebab-case, e.g. "create-record" — the host route and useIntegrationAction() address the handler by this exact string.

Typing

@ekanos/sdk is published separately from your package, so it cannot see your zod schema from the browser side. You supply TInput and TOutput yourself at the call site, typically by exporting z.infer types next to the schema (as above) and importing them into the widget. The hook treats the wire payload as unknown at runtime and checks only the response envelope.

PartnerActionDeclaration

The declaration type is exported from @ekanos/sdk/integration, so you can name it when you export a handler to share with a tool:
With zod 4 support (0.4.0), PartnerActionDeclaration’s schema generics are constrained by Standard Schema (StandardSchemaV1, exported from @ekanos/integration-schema), so a schema from either zod major satisfies them. Inside your own module the types infer precisely; in the erased actions record on the definition, an action handler’s input is any, so existing handlers like handler: (ctx, input) => createRecord(input) still compile.

Call an action from a widget

useIntegrationAction<TInput, TOutput>(slug, actionName, options) from @ekanos/sdk/hooks is a TanStack Query mutation that posts to POST /api/integrations/<slug>/actions/<actionName> with the body { accountId, input }.
mutate and mutateAsync take the action’s input directly. A resolved mutation’s data is TOutput. Options: integrationActionUrl(slug, actionName, { baseUrl? }) builds the exact URL the hook requests. It is exported from @ekanos/sdk/hooks only, because a test double needs to recognize the URL.

Errors

A rejected mutation’s error is always an IntegrationActionError: never a bare Error, a Response, or a raw fetch exception.
The response envelope is { success: true, data } on success and { success: false, error, code, issues? } on failure.

Showing your own message: PublicActionError

Every throw from a handler is logged server-side and collapses to a generic "Action failed." under handler_error, because a thrown error can embed vendor response bodies or credentials. To show your own message to the user verbatim, throw PublicActionError from @ekanos/sdk/integration:
The route surfaces message as-is under the same handler_error code and status. Only put text in it that is safe for the end user to read.

Calling a vendor from the browser

Actions run on the server. When a widget has to call a vendor API from the browser, use useIntegrationFetch() from @ekanos/sdk/hooks instead of bare fetch(). Outside a provider it is a plain globalThis.fetch passthrough. The harness mounts IntegrationFetchProvider and, in live mode, checks those requests against your egress list; production runs no egress check on the client path, so keep credentials server-side and call the vendor from an action or tool through ctx.fetch. See Live mode and Known limitations. useActivationData() and IntegrationActivationDataProvider give a widget activation data, but only the harness mounts a provider today. Outside one the hook returns null, so do not depend on it for production behavior. In widget tests, createMockFetchProvider() from @ekanos/sdk/testing returns a provider component that answers useIntegrationFetch() calls from the same fixtures as createMockContext(). See Testing.

Troubleshooting

  • action_not_found — the second argument to useIntegrationAction() does not match a key in actions.
  • invalid_input — the input failed the input schema; read error.issues.
  • handler_error with Action failed. — the handler threw. Throw PublicActionError if the user should see the message, and read server logs for the real error. In the harness, the ekanos dev terminal prints it.

Next steps

  • Harness actions — run actions locally in fixtures or live mode
  • Tools — the assistant-facing counterpart
  • Widgets — the Widget.* compound API