Build a Workflow
Run JavaScript on CRM events with the Clarify SDK
Workflows are available on request. Contact support@clarify.ai to enable them for your workspace.
A workflow runs a sequence of steps when something happens in your CRM. It starts with a trigger (a record being created, updated, or deleted, added to or removed from a list, an inbound webhook, a schedule, and more) and then runs its steps in order.
For a simple, linear automation you can chain no-code action blocks in the builder: create a record, update a record, send an email, wait, or call an outbound webhook. When a workflow needs real logic (conditionals, loops, validation, custom error handling, or several related reads and writes), reach for the Execute code step and write that logic in JavaScript. A common example is validation: when a deal moves to a new stage, check that the required fields are filled in and flag the record if they aren't.
Choose your workflow structure
No-code action blocks are the right fit when the workflow is short and sequential: a trigger, a handful of actions, and no branching beyond a trigger filter.
For anything more involved, a single Execute code step is usually the clearest way to structure a workflow. Keeping the logic in one place means the run logs point to one step, you can test it as a unit, and branching, loops, and error handling live in code rather than spread across a long chain of blocks. Action blocks have no per-block conditional gating: use trigger filters to decide whether a run starts, and Execute code for any branching after it does.
Use Execute code for workflow logic
Your code is an exported async function with a fixed signature:
export async function handler(input, { clarify, _, workflowContext }) {
// your code
return result;
}Clarify passes four values to your handler: your step's input, plus a context
object holding the rest.
| Argument | Description |
|---|---|
input | The step's own configured input |
clarify | A pre-authenticated Clarify SDK client for reading and writing records |
_ | lodash, for convenience |
workflowContext | The trigger data and the outputs of earlier steps |
Whatever you return is stored as the step's result and is available to later
steps. To fail the run, throw an error. The message shows up in the run logs.
Accessing the triggering record
The event that started the run lives on workflowContext.trigger.event:
| Path | Description |
|---|---|
workflowContext.trigger.event.object.data | The record that triggered the run (fields read directly, e.g. .amount) |
workflowContext.trigger.event.object._id | The triggering record's ID |
workflowContext.trigger.event.actor | Who made the change: { _id, entity, data } |
workflowContext.trigger.event.type | The trigger event type |
To read outputs from an earlier step, use its ID: workflowContext.blocks['my-step'].result.
Example: validate on stage change
This step checks that a deal has the required fields when it enters Closed Won, and writes a plain-language error back to the record if not.
export async function handler(input, { clarify, workflowContext }) {
const deal = workflowContext.trigger.event.object.data;
// Only act when the deal is in "Closed Won".
if (deal.stage !== "closed_won") {
return { skipped: true };
}
const missing = [];
if (!deal.amount) missing.push("amount");
if (!deal.close_date) missing.push("close date");
if (!deal.company_id) missing.push("company");
const validationError = missing.length
? `Cannot close: missing ${missing.join(", ")}`
: "";
await clarify.records.update("deal", deal._id, {
validation_error: validationError,
});
return { validationError };
}A workflow won't re-trigger itself: Clarify skips the run when the triggering change was made by that same workflow, so writing back to the record you're processing is safe. This only covers self-triggering. If your write fires a second workflow whose write fires this one again, that cross-workflow cycle isn't prevented. Watch for it when chaining workflows.
Clarify SDK
The clarify client is already authenticated for your workspace. Don't read an
API key or construct a client yourself. Use it for all CRM access; a raw fetch
to the Clarify API bypasses authentication and pagination handling.
Read with clarify.resources (responses resolve relationships):
const response = await clarify.resources.list({
type: "company",
filter: { industry: "technology" },
page: { limit: 50, offset: 0 },
});
const companies = response.data; // each has .id and .attributesWrite with clarify.records (create, update, delete, and their
createBulk / updateBulk / deleteBulk counterparts):
// Read one record by ID — fields are under .attributes
const { data } = await clarify.records.get("deal", dealId);
const amount = data.attributes.amount;
// Create
await clarify.records.create("task", {
name: "Follow up",
due_date: "2026-01-31",
});
// Update (partial — send only the fields you want to change)
await clarify.records.update("deal", dealId, { stage: "negotiation" });Trigger data (workflowContext.trigger.event.object.data) exposes fields
directly (deal.amount), but SDK responses nest them under .attributes
(response.data.attributes.amount). Entity slugs are always lowercase:
deal, company, person, or a custom object's slug.
Runtime and limits
Code steps run in an isolated sandbox with a few constraints worth knowing:
- Runtime: Node.js, with a 60-second budget per run. Prefer a few bulk
SDK calls over many small ones, and avoid reading a related record per row in
a loop; resolve relationships in one
clarify.resources.listcall instead. - Dependencies: only
lodash(injected as_) and theclarifySDK are available.importother npm packages and the step fails when it runs. Use the globalDatefor date math. - Return value: must be JSON-serializable. Convert a
Dateto a string (date.toISOString()) and avoidMap,Set, and class instances. - Failure:
throwto fail the run with a message in the logs. Return{ error: '...' }instead when the failure is expected: the step still succeeds, and you handle the outcome in code (action blocks can't branch on it).
Tips
- Keep the bulk of your logic in one Execute code step: it's easier to test and debug than the same behavior split across a chain of action blocks.
- Test against a single record (trigger a manual run) before enabling the workflow for live events.
- If a run fails, open its logs and check each step's state to see where the data or the code went wrong.
- Double-check that your trigger filters match exactly the records you intend to process.