Skip to main content

Overview

Webhook Actions let you run custom JavaScript whenever a Vitally Playbook fires. When the playbook triggers, Zudo receives the webhook, runs your script, and automatically updates account traits in Vitally based on the result. This gives you a way to automate logic that would otherwise be manual — calculating dates, transforming values, updating traits conditionally — all without writing a separate integration. Common use cases:
  • Auto-renewal — calculate and set the next contract expiration date when a subscription renews
  • Conditional trait updates — set traits only when certain account conditions are met
  • Data transformations — derive new values from existing traits (e.g., days until renewal)
  • Audit trails — stamp a date trait with today’s date when a specific event occurs

Setup

1

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 below)
    • Enabled — toggle to activate or deactivate the action
  5. Save the action.
2

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.
3

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.

Writing Your Script

Your script runs in a secure JavaScript sandbox. Two objects are available as inputs, and your script must return either a trait update object or null.

Available Variables

account

The full Vitally account object, fetched fresh from the Vitally API at the time the webhook fires. Use this to get the latest trait values.
// 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.
// 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:
// 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

FunctionDescriptionExample
addMonths(date, months)Add months to a dateaddMonths("2026-01-31", 1)"2026-02-28"
addDays(date, days)Add days to a dateaddDays("2026-01-15", 30)"2026-02-14"
addYears(date, years)Add years to a dateaddYears("2026-01-15", 1)"2027-01-15"
today()Get today’s date as a stringtoday()"2026-02-04"
now()Get current datetime (ISO)now()"2026-02-04T12:30:45.123Z"
parseDate(date)Parse any date to ISO formatparseDate("Jan 15, 2026")"2026-01-15"
isPast(date)Check if a date is in the pastisPast("2025-01-01")true
isFuture(date)Check if a date is in the futureisFuture("2027-01-01")true
diffDays(date1, date2)Days between two datesdiffDays("2026-01-01", "2026-01-15")14

Interval Helpers

FunctionDescriptionExample
intervalToMonths(interval)Convert interval name to monthsintervalToMonths("annual")12
addInterval(date, interval)Add a named interval to a dateaddInterval("2026-01-15", "annual")"2027-01-15"
Supported interval names for addInterval and intervalToMonths:
Interval nameMonths
monthly, month to month1
quarterly3
semi-annual, semiannual, bi-annual6
annual, yearly12
biennial, 2-year24
triennial, 3-year36

Returning a Result

Your script must return one of two things:

Update traits

Return an object with a traits key to update the account in Vitally:
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:
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.
// 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.
return {
  traits: {
    last_contacted_at: today(),
  },
};

Conditional Update

Only update a trait when a specific condition is true; skip otherwise.
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.
const expDate = account.traits?.current_contract_expiration;

if (!expDate) {
  return null;
}

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

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

Troubleshooting

ErrorCauseSolution
Webhook action not foundInvalid token in the URLCheck that the webhook URL is correct and hasn’t changed
Action is disabledThe action’s toggle is offEnable the action in Settings → Connections
Connection is disabledThe Vitally connection is offEnable the Vitally connection in Connections
Missing account in payloadMalformed Vitally webhookVerify the Playbook webhook step is configured correctly
Execution timed outYour script took more than 5 secondsSimplify the logic; remove any loops or heavy operations
Invalid date: ...A bad value was passed to a date helperCheck that the date string is valid before passing it
Debugging tips:
  • Use console.log in your script to trace values. 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 a minimal script that just returns { traits: { test_trait: today() } } to confirm the webhook is firing correctly 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 or passing values to helpers.
  • Return null instead of throwing. If required data is missing or conditions aren’t met, return null rather than letting the script error out.
  • Use account for the latest values. The payload snapshot may be slightly stale; prefer account.traits when you need the current state.
  • Keep scripts focused. One action per Playbook trigger is easier to debug than one script that tries to do everything.
  • Avoid logging sensitive data. Don’t log full account objects or traits that contain PII.
  • Test in staging first. Verify your Playbook and webhook work end-to-end in a non-production environment before activating in production.