> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zudo.so/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook Actions

> Run custom JavaScript when a Vitally Playbook fires to automatically update account traits.

Webhook Actions run custom JavaScript whenever a Vitally Playbook fires. Zudo receives the webhook, executes your script, and updates account traits in Vitally — no separate integration required.

**Common use cases:** auto-renewal date calculation, conditional trait updates, data transformations (e.g. days until renewal), and audit trail timestamps.

## Setup

<Steps>
  <Step title="Create the action in Zudo">
    1. Go to **Settings → Connections**.
    2. Find your Vitally connection and open it.
    3. Under **Playbook Webhook Actions**, click **+ Add Action**.
    4. Fill in the fields:
       * **Name** — a descriptive label (e.g., "Auto-Renewal Handler")
       * **Description** — what this action does (optional but recommended)
       * **JavaScript Code** — your custom logic (see [Writing Your Script](#writing-your-script) below)
       * **Enabled** — toggle to activate or deactivate the action
    5. Save the action.
  </Step>

  <Step title="Copy the webhook URL">
    After saving, each action gets a unique webhook URL. Click the copy button next to the action to copy it.

    The URL format is:

    ```
    https://your-domain.com/api/webhooks/vitally/{unique-token}
    ```

    Keep this URL — you'll paste it into Vitally in the next step.
  </Step>

  <Step title="Configure the Vitally Playbook">
    1. In Vitally, create or open the Playbook you want to use.
    2. Add a **Webhook** action step.
    3. Paste your webhook URL into the URL field.
    4. Set your trigger conditions as needed.
    5. Activate the Playbook.

    From this point on, every time the Playbook fires, Vitally will POST to your webhook URL and Zudo will run your script.
  </Step>
</Steps>

## Writing Your Script

Your script runs in a secure JavaScript sandbox with two input objects. It must return either a trait update object or `null`.

### Available Variables

#### `account`

The full Vitally account object, fetched fresh at the time the webhook fires. Use this for the latest trait values.

```javascript theme={null}
// Access traits via account.traits
const expDate = account.traits?.current_contract_expiration;
const interval = account.traits?.billing_interval;
const accountName = account.name;
```

#### `payload`

The raw webhook payload sent by Vitally. It contains the playbook context and a snapshot of the account at the time of the trigger.

```javascript theme={null}
// payload structure
{
  playbookId: "playbook-uuid",
  matchedAt: "2026-02-02T21:20:54.345Z",
  matchId: "unique-match-id",
  match: {
    account: {
      id: "vitally-account-uuid",
      // Traits are at the TOP LEVEL here — not nested under .traits
      current_contract_expiration: "2026-03-15",
      billing_interval: "annual",
    }
  }
}
```

**Important:** The `account` object nests traits under `account.traits.key`, but the `payload` object puts traits directly on `payload.match.account.key`. When accessing the same trait from either source:

```javascript theme={null}
// From account object (traits nested)
const expFromAccount = account.traits?.current_contract_expiration;

// From payload object (traits at top level)
const expFromPayload = payload.match?.account?.current_contract_expiration;
```

Use `account` when you need the most up-to-date value. Use `payload` when you need the value as it was at the moment the Playbook triggered.

## Helper Functions

The sandbox includes built-in helpers for common date and interval operations.

### Date Helpers

| Function                  | Description                      | Example                                       |
| ------------------------- | -------------------------------- | --------------------------------------------- |
| `addMonths(date, months)` | Add months to a date             | `addMonths("2026-01-31", 1)` → `"2026-02-28"` |
| `addDays(date, days)`     | Add days to a date               | `addDays("2026-01-15", 30)` → `"2026-02-14"`  |
| `addYears(date, years)`   | Add years to a date              | `addYears("2026-01-15", 1)` → `"2027-01-15"`  |
| `today()`                 | Get today's date as a string     | `today()` → `"2026-02-04"`                    |
| `now()`                   | Get current datetime (ISO)       | `now()` → `"2026-02-04T12:30:45.123Z"`        |
| `parseDate(date)`         | Parse any date to ISO format     | `parseDate("Jan 15, 2026")` → `"2026-01-15"`  |
| `isPast(date)`            | Check if a date is in the past   | `isPast("2025-01-01")` → `true`               |
| `isFuture(date)`          | Check if a date is in the future | `isFuture("2027-01-01")` → `true`             |
| `diffDays(date1, date2)`  | Days between two dates           | `diffDays("2026-01-01", "2026-01-15")` → `14` |

### Interval Helpers

| Function                      | Description                     | Example                                                |
| ----------------------------- | ------------------------------- | ------------------------------------------------------ |
| `intervalToMonths(interval)`  | Convert interval name to months | `intervalToMonths("annual")` → `12`                    |
| `addInterval(date, interval)` | Add a named interval to a date  | `addInterval("2026-01-15", "annual")` → `"2027-01-15"` |

**Supported interval names for `addInterval` and `intervalToMonths`:**

| Interval name                            | Months |
| ---------------------------------------- | ------ |
| `monthly`, `month to month`              | 1      |
| `quarterly`                              | 3      |
| `semi-annual`, `semiannual`, `bi-annual` | 6      |
| `annual`, `yearly`                       | 12     |
| `biennial`, `2-year`                     | 24     |
| `triennial`, `3-year`                    | 36     |

## Returning a Result

Your script must return one of two things:

### Update traits

Return an object with a `traits` key:

```javascript theme={null}
return {
	traits: {
		trait_key: "new_value",
		another_trait: 12345,
		date_trait: "2026-12-31",
	},
};
```

Trait keys should match the trait paths in Vitally (e.g., `current_contract_expiration`). Values can be strings, numbers, booleans, or ISO date strings.

### Skip the update

Return `null` when you don't want to make any changes:

```javascript theme={null}
return null;
```

Return `null` when conditions aren't met, required data is missing, or you want to skip a particular trigger.

## Code Examples

### Auto-Renewal: Calculate Next Expiration Date

Use this to automatically advance the contract expiration date when a subscription renews.

```javascript theme={null}
// Get current expiration from either source
const currentExp = account.traits?.current_contract_expiration || payload.match?.account?.current_contract_expiration;

const interval = account.traits?.billing_interval || payload.match?.account?.billing_interval || "annual";

if (!currentExp) {
	console.warn("No expiration date found, skipping");
	return null;
}

const newExpiration = addInterval(currentExp, interval);

return {
	traits: {
		current_contract_expiration: newExpiration,
		last_auto_renewal_at: now(),
	},
};
```

### Set a Date Trait to Today

Use this to stamp a date trait whenever a Playbook fires — useful for audit trails.

```javascript theme={null}
return {
	traits: {
		last_contacted_at: today(),
	},
};
```

### Conditional Update

Only update a trait when a specific condition is true; skip otherwise.

```javascript theme={null}
const autoRenew = account.traits?.auto_renew || payload.match?.account?.auto_renew;

if (autoRenew === true) {
	return {
		traits: {
			last_auto_renew_check: today(),
		},
	};
}

return null; // Skip if auto_renew is not true
```

### Calculate Days Until Renewal

Compute a numeric trait from an existing date trait.

```javascript theme={null}
const expDate = account.traits?.current_contract_expiration;

if (!expDate) {
	return null;
}

const daysUntil = diffDays(today(), expDate);

return {
	traits: {
		days_until_renewal: daysUntil,
	},
};
```

## Troubleshooting

| Error                        | Cause                                   | Solution                                                 |
| ---------------------------- | --------------------------------------- | -------------------------------------------------------- |
| `Webhook action not found`   | Invalid token in the URL                | Check that the webhook URL is correct and hasn't changed |
| `Action is disabled`         | The action's toggle is off              | Enable the action in **Settings → Connections**          |
| `Connection is disabled`     | The Vitally connection is off           | Enable the Vitally connection in **Connections**         |
| `Missing account in payload` | Malformed Vitally webhook               | Verify the Playbook webhook step is configured correctly |
| `Execution timed out`        | Your script took more than 5 seconds    | Simplify the logic; remove any loops or heavy operations |
| `Invalid date: ...`          | A bad value was passed to a date helper | Check that the date string is valid before passing it    |

**Debugging tips:**

* Use `console.log` in your script — logs appear in the execution stats view for the action.
* Check the action card in Settings for **trigger count**, **error count**, and the **last error** message.
* Start with `{ traits: { test_trait: today() } }` to confirm the webhook fires before adding complex logic.
* The code editor validates syntax before saving, but runtime errors only surface after a real trigger.

## Best Practices

* Validate inputs before using them — check for `null` or `undefined` before accessing nested properties.
* Return `null` instead of throwing when required data is missing.
* Prefer `account.traits` for the latest values; `payload` may be slightly stale.
* One action per Playbook trigger is easier to debug than one script that does everything.
* Don't log full account objects or traits that contain PII.
* Test in staging before activating in production.
