# Packaging Custom Actions

This guide explains how to package, test, publish, and install a reusable `hexabot-action-*` npm package: a small package manifest, an extension entry file, optional settings and i18n assets, a README, and npm installation from a Hexabot project.

### When to Package an Action

Package an action when:

* multiple Hexabot projects need the same action;
* the action integrates with a third-party API, internal platform, or shared service;
* you want versioned releases, changelogs, and controlled rollouts;
* you want to publish the extension through npm or the Hexabot Extension Library.

For a one-off project action, keep the source inside the application project and compile it into `dist/extensions/actions/**/*.action.js`. For a reusable action, create a separate npm package named `hexabot-action-<name>`.

### Runtime Discovery

At startup, Hexabot resolves dynamic providers before NestJS bootstraps. The API loads compiled action providers from these locations:

```
node_modules/@hexabot-ai/api/dist/extensions/actions/**/*.action.js
node_modules/hexabot-action-*/**/*.action.js
dist/extensions/actions/**/*.action.js
```

Runtime binding kinds used by action packages are discovered with the same package prefix:

```
node_modules/@hexabot-ai/api/dist/extensions/actions/**/*.binding.js
node_modules/hexabot-action-*/**/*.binding.js
dist/extensions/actions/**/*.binding.js
```

This means a reusable package must satisfy two rules:

1. The package name starts with `hexabot-action-`.
2. The published tarball contains compiled JavaScript files ending in `.action.js` and, when needed, `.binding.js`.

Do not publish only TypeScript source for Hexabot v3 action discovery. The runtime glob matches JavaScript files.

### Recommended Package Structure

```
hexabot-action-acme-ticket/
|-- README.md
|-- package.json
|-- tsconfig.json
|-- src/
|   |-- acme-create-ticket.action.ts
|   `-- index.ts
`-- test/
    `-- acme-create-ticket.action.spec.ts
```

After build:

```
dist/
|-- acme-create-ticket.action.js
|-- acme-create-ticket.action.d.ts
|-- index.js
`-- index.d.ts
```

The public channel packages include i18n assets because channels expose source-level settings and user-facing labels. For action packages, Zod `.meta({ title, description })` is enough for most editor labels. If you add translation files, confirm that the target Hexabot application is configured to load translations from the installed package; action provider discovery and i18n loading are separate concerns.

### `package.json`

Use npm metadata that makes the package discoverable and keep Hexabot runtime packages as peer dependencies. External clients used only by the action can be regular dependencies.

```json
{
  "name": "hexabot-action-acme-ticket",
  "version": "1.0.0",
  "description": "Create and update Acme support tickets from Hexabot workflows.",
  "author": "Your Organization",
  "license": "FCL-1.0-ALv2",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "files": [
    "dist",
    "README.md"
  ],
  "scripts": {
    "clean": "rimraf dist",
    "build": "tsc -p tsconfig.json",
    "typecheck": "tsc --noEmit -p tsconfig.json",
    "prepack": "npm run build",
    "pack:check": "npm pack --dry-run"
  },
  "peerDependencies": {
    "@hexabot-ai/api": "^3.2.0",
    "zod": "^4.0.0"
  },
  "devDependencies": {
    "@hexabot-ai/api": "^3.2.0",
    "@types/node": "^20.0.0",
    "rimraf": "^6.0.0",
    "typescript": "^5.1.0",
    "zod": "^4.0.0"
  },
  "keywords": [
    "hexabot",
    "hexabot-action",
    "workflow",
    "automation"
  ]
}
```

Use the Hexabot version range that matches the projects you support. If the action uses NestJS decorators directly, also add the required NestJS packages as peer dependencies. If the action only uses `createAction`, `@nestjs/common` is not required in the package source. Add `@hexabot-ai/agentic` only when your package imports it directly. Add third-party API clients under `dependencies`.

### `tsconfig.json`

Compile to CommonJS unless your target Hexabot application explicitly supports another module format.

```json
{
  "compilerOptions": {
    "target": "ES2021",
    "module": "commonjs",
    "moduleResolution": "node",
    "declaration": true,
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*.ts"]
}
```

Do not import Hexabot internals through the monorepo `@/` alias in an npm package. Use exports from `@hexabot-ai/api`, and use `@hexabot-ai/agentic` only when you need its public contracts. If a type or helper is not exported, treat it as an unstable internal API.

### Implement the Action

Use `createAction` for simple actions that can work with `input`, `settings`, `bindings`, and `context.services`. Extend `BaseAction` only when the action needs constructor injection or class-level helpers.

```ts
import { createAction } from '@hexabot-ai/api';
import { z } from 'zod';

const inputSchema = z.strictObject({
  subject: z.string().min(1).meta({
    title: 'Subject',
    description: 'Ticket subject.',
  }),
  customer_email: z.email().meta({
    title: 'Customer Email',
    description: 'Customer email address.',
  }),
  priority: z.enum(['low', 'normal', 'high']).default('normal').meta({
    title: 'Priority',
    description: 'Ticket priority in Acme.',
  }),
});

const settingsSchema = z.strictObject({
  base_url: z.url().default('https://api.acme.example').meta({
    title: 'Base URL',
    description: 'Acme API base URL.',
  }),
  credential_id: z.string().min(1).meta({
    title: 'Credential',
    description: 'Credential containing the Acme API token.',
    'ui:widget': 'AutoCompleteWidget',
    'ui:options': {
      entity: 'Credential',
      valueKey: 'id',
      labelKey: 'name',
      enableEntityAddButton: true
    }
  }),
});

const outputSchema = z.discriminatedUnion('success', [
  z.strictObject({
    success: z.literal(true),
    ticket_id: z.string(),
    status: z.int(),
  }),
  z.strictObject({
    success: z.literal(false),
    status: z.int().optional(),
    error: z.string(),
  }),
]);

export const AcmeCreateTicketAction = createAction({
  name: 'acme_create_ticket',
  description: 'Creates a support ticket in Acme.',
  group: 'acme',
  icon: 'Ticket',
  color: '#2f80ed',
  inputSchema,
  settingsSchema,
  outputSchema,
  async execute({ input, settings, context }) {
    const token = await context.services.credentials.findOneValue(
      settings.credential_id,
    );

    if (!token) {
      throw new Error('Missing Acme API credential value.');
    }

    const response = await fetch(`${settings.base_url}/tickets`, {
      method: 'POST',
      headers: {
        authorization: `Bearer ${token}`,
        'content-type': 'application/json',
      },
      body: JSON.stringify({
        subject: input.subject,
        customer_email: input.customer_email,
        priority: input.priority,
      }),
      signal:
        settings.timeout_ms && settings.timeout_ms > 0
          ? AbortSignal.timeout(settings.timeout_ms)
          : undefined,
    });

    if (!response.ok) {
      const errorText = await response.text();

      return {
        success: false,
        status: response.status,
        error: errorText.slice(0, 500) || 'Acme ticket request failed.',
      };
    }

    const payload = (await response.json()) as { id?: string };
    const ticketId = payload.id ? String(payload.id) : '';

    if (!ticketId) {
      return {
        success: false,
        status: response.status,
        error: 'Acme response did not include a ticket id.',
      };
    }

    return {
      success: true,
      ticket_id: ticketId,
      status: response.status,
    };
  },
});

export default AcmeCreateTicketAction;
```

Then export it from `src/index.ts`:

```ts
export * from './acme-create-ticket.action';
```

Action names should be stable `snake_case` identifiers. Workflows reference the name directly:

```yaml
defs:
  create_ticket:
    kind: task
    action: acme_create_ticket
    inputs:
      subject: =$input.subject
      customer_email: =$input.email
```

The raw action result is available to later tasks as `$output.create_ticket`.

### Settings, Credentials, and Secrets

Keep workflow YAML free of secrets. Store secret values as Hexabot credentials and use a setting field that references the credential record. Resolve the credential inside `execute()` with:

```ts
const token = await context.services.credentials.findOneValue(credentialId);
```

Do not log or return raw credential values, bearer tokens, full authorization headers, or sensitive upstream payloads.

Use action settings for runtime configuration such as API base URL, region, mode, default limits, or credential references. Do not redefine the base action settings `timeout_ms` or `retries`; Hexabot parses them for every action and passes them through `settings`.

### Optional Runtime Bindings

If the package introduces a reusable runtime binding kind, add a compiled `*.binding.js` file. For example:

```ts
import { createBindingKind } from '@hexabot-ai/api';
import { z } from 'zod';

export const AcmeAccountBindingKind = createBindingKind({
  kind: 'acme_account',
  schema: z.strictObject({
    account_id: z.string().min(1),
  }),
  multiple: false,
  color: '#2f80ed',
  icon: 'Building2',
});

export default AcmeAccountBindingKind;
```

Only add `supportedBindings` to the action metadata when the action actually reads from `bindings`.

### Local Package Testing

Build and inspect the package before publishing:

```sh
npm run typecheck
npm run build
npm pack --dry-run
```

Confirm that the pack output contains `dist/**/*.action.js` and does not contain unnecessary source maps, test fixtures, credentials, `.env` files, or local database files.

Install the packed tarball in a Hexabot project:

```sh
cd path/to/hexabot-project
npm install ../hexabot-action-acme-ticket/hexabot-action-acme-ticket-1.0.0.tgz
hexabot dev
```

With pnpm:

```sh
pnpm add ../hexabot-action-acme-ticket/hexabot-action-acme-ticket-1.0.0.tgz
pnpm dev
```

After the API starts, verify that the action is registered:

```sh
curl http://localhost:3000/api/workflow/actions
```

Look for an entry with `"name": "acme_create_ticket"`. If the action is missing, check these common issues:

* the package name does not start with `hexabot-action-`;
* the tarball does not contain compiled `*.action.js` files;
* the file name ends with `.js` but not `.action.js`;
* a dependency is missing from the consuming Hexabot project;
* another installed action already registered the same action name;
* the API was not restarted after installation.

### Publishing to npm

1. Update `README.md` with installation, configuration, credential setup, workflow examples, output contract, and troubleshooting.
2. Confirm package metadata: `name`, `version`, `description`, `license`, `repository`, `bugs`, `homepage`, `keywords`, and supported Hexabot version.
3. Run tests, typecheck, build, and `npm pack --dry-run`.
4. Install the generated tarball in a clean Hexabot project and verify `/api/workflow/actions`.
5. Publish:

```sh
npm login
npm publish --access public
```

The default loader scans `node_modules/hexabot-action-*`. Do not publish the runtime package only as `@scope/hexabot-action-name` unless the consuming application extends the dynamic provider patterns or otherwise exposes the compiled files under a discoverable `hexabot-action-*` package path. The standard community convention is an unscoped `hexabot-action-*` package.

### Submitting to the Hexabot Extension Library

After publishing to npm, prepare the catalog submission with:

* npm package name and version;
* public repository URL;
* README with setup and workflow examples;
* screenshots or short demo video when the action has visible editor behavior;
* list of required credentials, environment variables, and external accounts;
* compatibility range for Hexabot and Node.js;
* license and attribution;
* support or issue tracker link.

The extension should be installable with a single package-manager command from a Hexabot project:

```sh
npm install hexabot-action-acme-ticket
```

Restart the API after installation so dynamic provider discovery can register the action.

### Release Checklist

* Package name starts with `hexabot-action-`.
* Published files include compiled `dist/**/*.action.js`.
* Optional binding providers compile to `dist/**/*.binding.js`.
* Action names are stable, unique, and `snake_case`.
* Zod input, output, and settings schemas are explicit.
* Outputs are JSON-serializable and safe for workflow references.
* Secrets are referenced through Hexabot credentials, not workflow YAML.
* README documents install, configuration, workflow usage, outputs, and errors.
* `npm pack --dry-run` shows only expected files.
* A clean Hexabot project can install the package and see the action at `/api/workflow/actions`.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.hexabot.ai/developer-guide/develop-custom-actions/packaging-custom-actions.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
