The ctx object
ctx is the second argument to your handler. It’s the one addition Turbyn makes to HubSpot’s signature, and it’s entirely optional.
exports.main = async (event, ctx) => { // ctx.hubspot: pre-authenticated CRM client // ctx.secrets: decrypted secret values // ctx.store: key-value store, atomic // ctx.log: structured logging // ctx.env: portal and run metadata // ctx.retry: retry helper for flaky calls};ctx.hubspot
Section titled “ctx.hubspot”A CRM client already authenticated as the portal that triggered the run. No token handling, no client construction.
const contact = await ctx.hubspot.contacts.get(event.object.objectId);
await ctx.hubspot.companies.update(companyId, { industry: "Software", numberofemployees: "250",});
const results = await ctx.hubspot.contacts.search({ filterGroups: [ { filters: [{ propertyName: "email", operator: "EQ", value: "j@example.com" }], }, ],});Rate limits are handled for you. The client backs off and retries on a 429 rather than surfacing it. Because your run isn’t racing a 20-second timeout, that backoff is affordable.
ctx.secrets
Section titled “ctx.secrets”Values from the secrets vault, decrypted at invocation and injected into the run.
const stripe = new Stripe(ctx.secrets.STRIPE_API_KEY);Secrets are encrypted at rest, never returned to the browser after being saved, and excluded from logs. They exist for the lifetime of the run and nothing else.
Referencing a secret that doesn’t exist gives undefined rather than throwing. Check before use if it’s required.
ctx.store
Section titled “ctx.store”A key-value store scoped to your portal. Operations are atomic, which matters because workflow actions run concurrently across enrolled records.
await ctx.store.set("last-sync", new Date().toISOString());const last = await ctx.store.get("last-sync");
// Atomic. Safe when 200 records are enrolled at once.const n = await ctx.store.increment("processed-count");
await ctx.store.delete("last-sync");Use it for counters, round-robin assignment cursors, deduplication markers and cross-run state. It is not a database: no queries, no indexes, no scans. Keys are strings; values are JSON-serialisable.
Round-robin assignment is the canonical case:
const reps = ["alice", "bob", "carol"];const n = await ctx.store.increment("rr-cursor");const owner = reps[n % reps.length];Because increment is atomic, two records enrolled simultaneously get different reps. A read-then-write would hand both the same one.
ctx.log
Section titled “ctx.log”Structured logging. Output streams to the run console live, and is retained with the run.
ctx.log.info("enriching", { domain });ctx.log.warn("no match found", { domain });ctx.log.error("provider rejected the request", { status: res.status });console.log is captured too, so pasted code that uses it still works. ctx.log is preferable because the second argument stays structured and filterable rather than being stringified.
ctx.env
Section titled “ctx.env”Read-only metadata about the run.
ctx.env.portalId; // 12345678ctx.env.actionId; // the saved action's idctx.env.versionId; // which published version is executingctx.env.executionId; // this run; useful as an idempotency keyctx.env.isDryRun; // true during an editor test runisDryRun is useful for guarding an external side effect the interceptor can’t know about:
if (!ctx.env.isDryRun) { await sendSlackMessage(summary);}Turbyn intercepts HubSpot writes during a dry run, but it can’t intercept a call to someone else’s API. Guard those yourself.
ctx.retry
Section titled “ctx.retry”Retry with exponential backoff, for flaky third parties.
const data = await ctx.retry( async () => { const res = await fetch("https://api.example.com/enrich"); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); }, { attempts: 3, backoffMs: 1000 },);Only worth using where a retry is genuinely safe. Retrying a non-idempotent write can duplicate it. Combine with ctx.env.executionId as an idempotency key when the destination supports one.
Python
Section titled “Python”Identical surface, Python conventions:
async def main(event, ctx): contact = await ctx.hubspot.contacts.get(event["object"]["objectId"])
await ctx.store.increment("processed-count") ctx.log.info("processed", {"id": contact["id"]})
return {"outputFields": {"output1": "ok"}}