[object Object]

Custom code actions in workflows changed what HubSpot can do. They run in a Node.js sandbox, get up to 20 seconds, and have access to a hubspotClient instance. Here are six patterns I’ve shipped in production.

1. Round-robin assignment with availability check

exports.main = async (event, callback) => {
  const reps = ["123", "456", "789"];
  const ooo = await getOOOReps(); // call your custom API
  const available = reps.filter(r => !ooo.includes(r));
  const next = available[Date.now() % available.length];
  callback({ outputFields: { assigned_rep: next } });
};

Native round-robin doesn’t skip OOO reps. This does.

2. Multi-currency normalization

exports.main = async (event, callback) => {
  const rates = await fetch("https://api.exchangerate.host/latest?base=USD")
    .then(r => r.json());
  const usd_amount = event.fields.amount / rates.rates[event.fields.currency];
  callback({ outputFields: { amount_usd: usd_amount } });
};

Pipe deal amount in any currency to a normalized USD field for forecasting.

3. External enrichment fallback

When your enrichment provider misses a company, hit a secondary API. Don’t gate the workflow on the first call succeeding.

4. Slugify a property for URL use

exports.main = (event, callback) => {
  const slug = event.fields.title
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-|-$/g, "");
  callback({ outputFields: { slug } });
};

Drives the URL of programmatically-generated CMS pages from a custom object.

5. Conditional sequencing across deals

When a contact has 3+ deals stuck in “Negotiation” for 30 days, escalate. The UI can express the count but not “across all deals.” Custom code can.

6. Webhook with retry and circuit breaker

exports.main = async (event, callback) => {
  for (let attempt = 0; attempt < 3; attempt++) {
    try {
      const res = await fetch(targetUrl, { method: "POST", body: JSON.stringify(event.fields) });
      if (res.ok) return callback({ outputFields: { synced: true } });
    } catch (e) { /* retry */ }
    await new Promise(r => setTimeout(r, 1000 * (attempt + 1)));
  }
  callback({ outputFields: { synced: false, error: "max_retries" } });
};

Native webhook actions don’t retry. This pattern saves you the integration headaches.

Three rules that keep custom code maintainable

  1. Keep each action under 60 lines. Anything longer belongs in a serverless function.
  2. Log to console.log; the action’s execution log is your debugger.
  3. Always set output fields, never throw. Throws halt the workflow without telling you why.

What to do this week

Audit your most complex multi-action workflow. If you have a chain of 5+ “if/then” branches, replace it with one custom code action. Maintenance load drops 70%.

[object Object]
Share