> For the complete documentation index, see [llms.txt](https://docs.chord.co/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.chord.co/activate/event-streaming/functions.md).

# Functions

Functions are small pieces of JavaScript that run on each event before Chord sends it to a server destination. Use them to change an event, add to it, split it or drop it.

Some things functions are good for:

* **Filtering**: drop events you don't want, such as test traffic or internal visits.
* **Normalizing**: make event data consistent, for example turn `page_view`, `pageview` and `pageView` into one name.
* **Enriching**: add data from another service, such as a customer's loyalty tier.
* **Routing**: send an event to a different table based on its properties.

{% hint style="info" %}
Functions only run for server destinations. They don't run for browser destinations, which send events straight from the visitor's browser. See [Browser and server destinations](/activate/event-streaming/browser-and-server-destinations.md).
{% endhint %}

For changes that don't need code, such as renaming a property, use [property mappings](/activate/event-streaming/property-mappings.md). Mappings run before functions.

## What a function looks like

Every function has this shape:

```js
export default async function(event, context) {
  // Your logic here
  return event; // or null to drop the event
}
```

**event** is the event being processed. It usually has these fields:

* `event`: the event name, such as "Order Completed".
* `type`: the event type, such as `track`, `page` or `identify`.
* `userId` and `anonymousId`: who did it.
* `timestamp`: when it happened, in ISO 8601 format.
* `properties`: the event's own data.
* `context`: details such as the page, IP address and browser.

**context** gives your function a few helpers:

* `context.log.debug`, `info`, `warn` and `error` write to the function's logs.
* `context.fetch(url, options)` makes web requests, using the standard Fetch API.
* `context.store` is a key-value store that keeps data between runs.

### What a function returns

* **An object**: this becomes the event Chord sends on.
* **An array of objects**: each object becomes its own event. Only the last function in a pipeline can return an array.
* **`null`, `false` or `[]`**: Chord drops the event.
* **Nothing**: Chord uses the original event, with any changes you made to it.

## Create a function

1. In Chord, go to **CDP** and open the **Functions** tab.
2. Click **Create Your First Function**, or **+ Function** if you already have some.
3. Click the function name at the top left, type a name and click the check mark.
4. Write your code in the editor.
5. Click **Save**.

To edit a function, click the pencil icon next to it in the Actions column. To delete one, open the three-dot menu in the Actions column and click the trash icon.

## Test and debug

The editor has a **Run** button that runs your function on sample data. Two tabs show what happened:

* **Logs** shows anything you wrote with `context.log`. Use it to print event contents and trace your logic.
* **Last Run Result** shows what the function returned. You can see whether it dropped the event, changed it or passed it on, along with any errors.

## Add a function to a connection

Functions run on connections between a source and a destination.

1. On the **Overview** tab, open the destination's menu and click **View Connected Streams**.
2. Click the pencil icon next to the connection.
3. Under **Functions pipeline**, add functions from your library. They run in the order you list them.
4. Click **Save changes**.

You can also set environment variables on a connection, below the function library. Read them in your code like this:

```js
process.env.VARIABLE_NAME
```

## Persistent storage

The `store` object keeps data between runs. Use it for things like rate limits, caching and keeping state across events.

```js
// Get a value (undefined if it's missing or expired)
const value = await store.get("my-key");

// Set a value. It expires after 31 days by default.
await store.set("my-key", { foo: "bar" });

// Set a value with your own expiry
await store.set("my-key", { foo: "bar" }, "1h");   // 1 hour
await store.set("my-key", { foo: "bar" }, "7d");   // 7 days
await store.set("my-key", { foo: "bar" }, 3600);   // 3600 seconds
await store.set("my-key", { foo: "bar" }, "inf");  // never expires

// Delete a value
await store.del("my-key");

// Seconds left: a positive number, -1 for no expiry, -2 if the key doesn't exist
const ttl = await store.ttl("my-key");
```

The expiry (TTL) can be a number of seconds, text such as `"30s"`, `"5m"`, `"2h"` or `"7d"`, or `"inf"` for no expiry.

Here is a full example:

```js
export default async function(event, { log, fetch, store }) {
  let key = "my-key";
  let ttl = "10s";

  // Look for the value in the cache
  let value = await store.get(key);

  // If it's there, log it. If not, save a new value that expires in 10 seconds.
  if (value) {
    log.info(`Cache hit! Value is ${value}`);
  } else {
    let newValue = Math.random().toString(36).substring(2);
    log.info(`Cache miss! Adding ${newValue} to cache; TTL ${ttl}.`);
    await store.set(key, newValue, ttl);
  }
}
```

### Write for an empty store

The store can be empty at any time. A key might not exist, might have expired, or the store might be briefly unavailable. When that happens, your function keeps running. `get()` returns `undefined`, and `set()` and `del()` do nothing. Chord logs a warning.

Always handle a missing value:

```js
// Good: handles a miss
const cached = await ctx.store.get("my-key");
if (!cached) {
  const freshData = await fetchData();
  await ctx.store.set("my-key", freshData, "1h");
  return processEvent(event, freshData);
}
return processEvent(event, cached);

// Avoid: assumes the value is always there
const cached = await ctx.store.get("my-key");
return processEvent(event, cached.someProperty); // fails if cached is undefined
```

### Keep your keys unique

Every function in your workspace shares the same store. One function can read, change or delete another's keys. Start each key with a prefix for its function:

```js
// Good: prefixed keys
await store.set("rate-limiter:user:123", count);
await store.set("enrichment-cache:company:456", data);

// Avoid: generic keys that might collide
await store.set("user:123", count);
await store.set("cache:456", data);
```

If you run two versions of a function, such as development and production, add an environment variable to the prefix:

```js
// Set FUNCTION_ENV as an environment variable: "dev" or "prod"
const keyPrefix = process.env.FUNCTION_ENV || "dev";

await store.set(`${keyPrefix}:rate-limit:${userId}`, count);
```

## Examples

**Drop events with no user ID**

```js
export default async function(event, context) {
  if (!event.userId) {
    context.log.debug(`Dropped event without userId: ${event.event}`);
    return null;
  }
  return event;
}
```

**Only forward some events**

```js
export default async function(event, context) {
  const allowedEvents = [
    "Order Completed",
    "Product Viewed",
    "Cart Updated",
  ];

  if (!allowedEvents.includes(event.event)) {
    context.log.debug(`Dropped disallowed event: ${event.event}`);
    return null;
  }

  return event;
}
```

**Add data from an outside service**

```js
export default async function(event, context) {
  try {
    const userId = event.userId;

    if (!userId) {
      return event; // Skip enrichment if there is no userId
    }

    const apiKey = process.env.API_KEY;
    const apiBaseUrl = process.env.API_BASE_URL;

    if (!apiKey) {
      context.log.warn("API_KEY not configured, skipping enrichment");
      return event;
    }

    const response = await context.fetch(`${apiBaseUrl}/users/${userId}`, {
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
      },
    });

    if (response.ok) {
      const userData = await response.json();
      event.properties.lifetimeValue = userData.ltv;
      context.log.info(`Enriched user ${userId} with external data`);
    } else {
      context.log.warn(`API returned ${response.status}: ${response.statusText}`);
    }
  } catch (error) {
    context.log.error(`Failed to enrich event: ${error.message}`);
    // Keep going even if enrichment fails
  }

  return event;
}
```

## Errors

A function can throw a specific error to tell Chord what to do with the event. Each error changes whether the event is sent, whether Chord retries, and whether Chord sends a [notification](/activate/event-streaming/notifications.md).

| What the function does                       | Event sent to the destination                   | Retried | Notification |
| -------------------------------------------- | ----------------------------------------------- | ------- | ------------ |
| `return null`, `return false` or `return []` | No                                              | No      | No           |
| `throw new Error(...)`                       | Yes                                             | No      | Yes          |
| `throw new NoRetryError(...)`                | No                                              | No      | Yes          |
| `throw new RetryError(...)`                  | Yes, with whatever the function had done so far | Yes     | Yes          |
| `throw new RetryError(..., { drop: true })`  | No                                              | Yes     | Yes          |

To drop an event on purpose, return a value. Don't throw an error. A returned `null` makes no notification. See [Errors and retries](/activate/event-streaming/errors-and-retries.md) for what happens after a retry.

**Drop an event on purpose.** Return `null`, `false` or an empty array. Chord records the event as dropped, logs no error and sends no notification.

```js
export default async function(event, context) {
  if (event.properties.internal) {
    return null; // quietly dropped, no notification
  }
  return event;
}
```

**Standard `Error`.** Use this for a bug or surprise in your code. The event still goes on to the destination, but Chord records the error and sends a notification. If you want events to keep flowing without a notification, catch the error in your code instead.

```js
export default async function(event, context) {
  const result = await context.fetch("https://api.example.com/enrich", {
    method: "POST",
    body: JSON.stringify({ userId: event.userId }),
  });
  if (!result.ok) {
    throw new Error(`Enrichment API returned ${result.status}`);
  }
  return event;
}
```

**`NoRetryError`.** Use this when the event itself has a problem that retrying can't fix, such as a missing required field. Chord drops the event and sends a notification. Don't use it just to filter events.

```js
export default async function(event, context) {
  if (!event.properties.order_id) {
    throw new NoRetryError("order_id is required — event cannot be delivered");
  }
  return event;
}
```

**`RetryError`.** Use this for a temporary problem, such as an outside service being down. Chord retries the event later and sends a notification.

```js
export default async function(event, context) {
  const res = await context.fetch("https://api.example.com/enrich", {
    method: "POST",
    body: JSON.stringify({ userId: event.userId }),
  });
  if (res.status === 503) {
    throw new RetryError("Enrichment service temporarily unavailable");
  }
  event.properties.enriched = await res.json();
  return event;
}
```

By default, Chord still sends the event on with whatever data was ready when the error happened. The retry then runs the function again, so the destination can get the event twice. To hold the event back until the retry works, pass `{ drop: true }`:

```js
throw new RetryError("Enrichment required but service is down", { drop: true });
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

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

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.chord.co/activate/event-streaming/functions.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

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.
