> Part of the walkerOS documentation. Project overview and full index: <https://www.walkeros.io/llms.txt>

# Create Your Own Destination

This guide provides the essentials for building a custom walkerOS destination.

## What is a destination?[​](#what-is-a-destination "Direct link to What is a destination?")

A destination is a function that receives events from walkerOS and sends them to an external service, such as an analytics platform, an API, or a database.

## The destination interface[​](#the-destination-interface "Direct link to The destination interface")

A destination is an object that implements the `Destination` interface. The most important property is the `push` function, which is called for every event.

```
interface Destination<Settings = unknown> {
config: {};
push: PushFn<Settings>;
type?: string;
init?: InitFn<Settings>;
on?(
event: 'consent' | 'session' | 'ready' | 'run',
context?: unknown,
): void | Promise<void>;
}
```

## The `push` function[​](#the-push-function "Direct link to the-push-function")

The `push` function is where you'll implement the logic to send the event to your desired service. It receives the `event` and a `context` object.

```
type PushFn<Settings> = (
  event: WalkerOS.Event,
  context: Destination.PushContext<Settings>,
) => void;
```

The push context contains:

* **`config`**: Destination configuration with settings
* **`env`**: Environment with window, document, etc.
* **`logger`**: Logger instance
* **`id`**: Unique destination identifier
* **`data`**: Pre-computed data from mapping
* **`rule`**: The matching mapping rule for this event
* **`ingest`**: Optional request metadata from source

## Example: A simple webhook destination[​](#example-a-simple-webhook-destination "Direct link to Example: A simple webhook destination")

Here is an example of a simple destination that sends events to a webhook URL.

```
import type { Destination } from '@walkeros/core';

// 1. Define your settings interface
interface WebhookSettings {
url: string;
}

// 2. Create the destination object
export const destinationWebhook: Destination<WebhookSettings> = {
type: 'webhook',
config: {},

push(event, { config }) {
const { settings } = config;

// 3. Access your settings
if (!settings?.url) return;

// 4. Send the event
fetch(settings.url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event),
}).catch(console.error);
},
};
```

## Schema validation (optional)[​](#schema-validation-optional "Direct link to Schema validation (optional)")

Destinations can export Zod schemas to provide runtime validation and TypeScript IDE support for configuration options. Export a `schemas` namespace containing `SettingsSchema` and `MappingSchema` to enable validation, autocomplete, and JSON Schema generation for tools like MCP and Explorer.

See the [Destination Schemas Guide](https://github.com/elbwalker/walkerOS/blob/main/DESTINATION_SCHEMAS.md) for implementation details, or reference existing destinations like [Meta Pixel](https://github.com/elbwalker/walkerOS/tree/main/packages/web/destinations/meta) for complete examples.

## The `on` method (Optional)[​](#the-on-method-optional "Direct link to the-on-method-optional")

The optional `on` method allows your destination to respond to collector lifecycle events. This is useful for handling consent changes, session management, or cleanup tasks.

### Available Events[​](#available-events "Direct link to Available Events")

* **`consent`** - Called when user consent changes, with consent state as context
* **`session`** - Called when a new session starts, with session data as context
* **`ready`** - Called when the collector is ready to process events
* **`run`** - Called when the collector starts or resumes processing

### Example: Consent-Aware Destination[​](#example-consent-aware-destination "Direct link to Example: Consent-Aware Destination")

```
export const destinationWithConsent: Destination<WebhookSettings> = {
type: 'webhook-consent',
config: {},

on(event, context) {
if (event === 'consent') {
console.log('Consent updated:', context);
// React to consent changes - maybe clear cookies if consent withdrawn
}
},

push(event, { config }) {
console.log('Event:', event);
},
};
```

## Setup lifecycle (optional)[​](#setup-lifecycle-optional "Direct link to Setup lifecycle (optional)")

Components may implement an optional `setup()` lifecycle to provision external resources (BigQuery datasets, Pub/Sub topics, SQLite tables, webhook registrations) before the runtime ever processes events. Setup is **operator-time**: it runs only when an operator explicitly invokes `walkeros setup <kind>.<name>`. The runtime never auto-invokes it.

This separation isolates one-shot infrastructure provisioning from the high-volume event hot path. Production flows can ship with restricted runtime credentials (write-only) while operators run setup with elevated credentials (create) once per environment.

The same lifecycle applies to sources, destinations, and stores. Transformers are pure functions and have no setup.

### Behavior[​](#behavior "Direct link to Behavior")

* Triggered only by `walkeros setup <kind>.<name>` (per-component, explicit). Never by `push`, `simulate`, `deploy`, or the runtime.
* Setup is opt-in via `config.setup` in the flow config. The CLI invocation requires it to be `true` or an object; `false` or omitted means the operator's setup invocation is a narrated skip.
* Idempotency is the package's responsibility. Re-running setup against a fully provisioned environment is a safe no-op.
* Structured JSON output is opt-in via `--json` (e.g. `walkeros setup <kind>.<name> --json`). In normal mode the CLI narrates the action and never splices a JSON envelope into the output, even if `setup()` returns a value. The opt-in setup behavior and idempotency are independent of `--json`.

### Example: a destination with setup[​](#example-a-destination-with-setup "Direct link to Example: a destination with setup")

```
import type { Destination } from '@walkeros/core';

interface BigQuerySettings {
  projectId: string;
  datasetId: string;
  tableId: string;
}

export const destinationBigquery: Destination<BigQuerySettings> = {
  type: 'bigquery',
  config: {},

  async setup({ config }) {
    const { settings } = config;
    if (!settings) return;
    // Create dataset and table if missing. Idempotent: re-running is a no-op.
    // Return any structured result the operator may want to inspect.
    return { datasetCreated: settings.datasetId, tableCreated: settings.tableId };
  },

  push(event, { config }) {
    // Hot path: only writes rows, never creates resources.
  },
};
```

The corresponding flow config opts in via `config.setup`:

```
{
  "destinations": {
    "bigquery": {
      "package": "@walkeros/server-destination-bigquery",
      "config": {
        "setup": true,
        "settings": { "projectId": "my-proj", "datasetId": "events", "tableId": "raw" }
      }
    }
  }
}
```

Then the operator runs:

```
walkeros setup destination.bigquery
```

### See also[​](#see-also "Direct link to See also")

* [walkeros setup CLI command](https://www.walkeros.io/docs/apps/cli.md#setup-command) for the operator-facing command, options, and exit codes.
* [Create your own source](https://www.walkeros.io/docs/sources/create-your-own.md) for source setup.
* [Stores](https://www.walkeros.io/docs/stores.md) for store setup and lifecycle ordering.

## Conditional activation with `require`[​](#conditional-activation-with-require "Direct link to conditional-activation-with-require")

Destinations can use `require` to delay registration until specific events fire. This prevents SDK loading and network requests before conditions are met:

```
await startFlow({
  destinations: {
    ga4: {
      code: ga4Destination,
      config: {
        require: ['session'],
        consent: { marketing: true },
      },
    },
  },
});
```

`require` and `consent` compose: `require` gates initialization before session information is available, `consent` gates every push (filter events by consent state).

## Using your destination[​](#using-your-destination "Direct link to Using your destination")

To use your custom destination, add it to the `destinations` object in your collector configuration.

```
import { startFlow } from '@walkeros/collector';
import { destinationWebhook } from './destinationWebhook';

const { elb } = await startFlow({
destinations: {
myWebhook: {
destination: destinationWebhook,
config: {
  settings: {
    url: 'https://api.example.com/events',
  },
},
},
},
});
```

## Advanced example: Session management[​](#advanced-example-session-management "Direct link to Advanced example: Session management")

Here's a more advanced example that demonstrates session handling and cleanup:

```
export const destinationWithSession: Destination<WebhookSettings> = {
type: 'webhook-session',
config: {},

on(event, context) {
switch (event) {
case 'session':
  // New session started
  console.log('New session:', context);
  // Could initialize session-specific tracking
  break;

case 'consent':
  // Handle consent changes
  const consent = context as { marketing?: boolean; analytics?: boolean };
  if (!consent?.marketing) {
    // Clear marketing-related data if consent withdrawn
    console.log('Marketing consent withdrawn, clearing data');
  }
  break;

case 'ready':
  // Collector is ready
  console.log('Starting destination services');
  break;

case 'run':
  // Collector resumed processing
  console.log('Collector resumed, processing queued events');
  break;
}
},

push(event, { config }) {
// Regular event processing
const { settings } = config;
if (!settings?.url) return;

fetch(settings.url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event),
}).catch(console.error);
},
};
```

## TypeScript integration[​](#typescript-integration "Direct link to TypeScript integration")

To get full TypeScript support for your destination's configuration, you can extend the `WalkerOS.Destinations` interface.

```
// types.ts
import type { Destination } from '@walkeros/core';
import type { WebhookSettings } from './destinationWebhook';

declare global {
namespace WalkerOS {
interface Destinations {
webhook: Destination.Config<WebhookSettings>;
}
}
}
```

## Environment dependencies (testing)[​](#environment-dependencies-testing "Direct link to Environment dependencies (testing)")

The `env` parameter enables dependency injection for external APIs and SDKs. This allows you to test your destination logic without making actual API calls or requiring real browser globals.

**Use Cases:**

* Mock external SDKs (Google Analytics, Facebook Pixel, AWS SDK)
* Test without network requests
* Simulate different API responses
* Run tests in any environment (Node.js, browser, CI)

### Defining an Environment[​](#defining-an-environment "Direct link to Defining an Environment")

Define the external dependencies your destination needs:

```
// types.ts - Web destination
import type { DestinationWeb } from '@walkeros/web-core';

export interface Env extends DestinationWeb.Env {
window: {
gtag: (command: string, ...args: unknown[]) => void;
};
}

// types.ts - Server destination
import type { DestinationServer } from '@walkeros/server-core';
import type { BigQuery } from '@google-cloud/bigquery';

export interface Env extends DestinationServer.Env {
BigQuery?: typeof BigQuery;
}
```

### Using Environment in Your Destination[​](#using-environment-in-your-destination "Direct link to Using Environment in Your Destination")

Use the 3rd generic parameter for type safety, then access env in `init` or `push`:

```
import type { DestinationWeb } from '@walkeros/web-core';
import { getEnv } from '@walkeros/web-core';

interface Settings {
/* ... */
}
interface Mapping {
/* ... */
}
interface Env extends DestinationWeb.Env {
window: { customAPI: (event: string) => void };
}

// Add Env as 3rd generic parameter for proper typing
export const destination: DestinationWeb.Destination<Settings, Mapping, Env> = {
type: 'custom',
config: {},

async init({ config, env }) {
// Initialize SDK using env, falls back to real APIs
const { window } = getEnv<Env>(env);
window.customAPI('init');
return config;
},

push(event, { config, env }) {
const { window } = getEnv<Env>(env);
window.customAPI(event.name);
},
};
```

### Creating Test Environments[​](#creating-test-environments "Direct link to Creating Test Environments")

Create reusable mock environments in an `examples/env.ts` file:

```
// examples/env.ts
import type { Env } from '../types';

export const push: Env = {
window: {
customAPI: jest.fn(),
},
};
```

Export from your examples index:

```
// examples/index.ts
export * as env from './env';
```

### Using in Tests[​](#using-in-tests "Direct link to Using in Tests")

```
import { clone } from '@walkeros/core';
import type { Destination, Collector } from '@walkeros/core';
import { createMockLogger } from '@walkeros/core';
import { examples } from './index';

// Helper to create push context for testing
function createPushContext(
  overrides: Partial<Destination.PushContext<Settings>> = {},
): Destination.PushContext<Settings> {
  return {
    config: {},
    env: examples.env.push,
    logger: createMockLogger(),
    id: 'test-destination',
    collector: {} as Collector.Instance,
    data: {},
    rule: undefined,
    ...overrides,
  };
}

describe('My Destination', () => {
  it('calls custom API', async () => {
    // Clone the example env to avoid mutations
    const testEnv = clone(examples.env.push);

    const context = createPushContext({ env: testEnv });
    await destination.push(event, context);

    expect(testEnv.window.customAPI).toHaveBeenCalledWith('page view');
  });
});
```

**Key Points:**

* **Production**: No `env` needed, uses real APIs (window, fetch, SDKs)
* **Testing**: Provide `env` with mocks for isolated testing
* **Type Safety**: 3rd generic parameter gives full autocomplete
* **Fallback**: `getEnv(env)` automatically uses real APIs if env not provided
* **Reusable**: Store mock environments in `examples/env.ts` for consistency
* **Context helper**: Use `createPushContext()` to standardize test context with `id` and `rule`

## Package convention[​](#package-convention "Direct link to Package convention")

Every walkerOS package includes machine-readable metadata for tooling and discovery.

### `walkerOS` field in package.json[​](#walkeros-field-in-packagejson "Direct link to walkeros-field-in-packagejson")

```
{
  "walkerOS": { "type": "destination", "platform": "web" },
  "keywords": ["walkeros", "walkeros-destination"]
}
```

| Field      | Required | Description                                |
| ---------- | -------- | ------------------------------------------ |
| `walkerOS` | Yes      | Object with `type` and `platform` metadata |

### Build-time generation[​](#build-time-generation "Direct link to Build-time generation")

Use `buildDev()` from the shared tsup config to auto-generate `walkerOS.json`:

```
import { buildDev } from '@walkeros/config/tsup';
```

This file contains your package's JSON Schemas and examples, enabling MCP tools and the CLI to validate configurations without installing your package.

### Optional: Hints[​](#optional-hints "Direct link to Optional: Hints")

Packages can export a `hints` record from `src/dev.ts` to provide lightweight, actionable context beyond schemas and examples, such as authentication methods, storage behavior, or troubleshooting tips. Hints are serialized into `walkerOS.json` and surfaced via MCP tools. See the `walkeros-create-destination` skill for details.

## Publishing checklist[​](#publishing-checklist "Direct link to Publishing checklist")

* [ ] `walkerOS` field in package.json
* <!-- -->
  Keywords include `walkeros` and `walkeros-destination`
* [ ] `buildDev()` in tsup.config.ts
* [ ] `dist/walkerOS.json` generated on build
* [ ] `npm run test` passes
* [ ] `npm run lint` passes
