Functions
Let your own code act on a result — the response names the function and its params, and the TypeScript and Python SDKs call the handler you registered, in order and once per call.
A function destination runs nothing on Dcision's side. The response names a function and the params to call it with, and your code — usually through the SDKs — calls the matching handler. Use it when the next step lives in the process that called the decision: assign an owner, write to your database, call an internal service.
Configure it
{
"key": "assign_owner",
"type": "function",
"when": { "conditions": [{ "field": "route", "on": "output", "operator": "eq", "value": "sales" }] },
"params": {
"email": { "from": "state.email", "required": true },
"route": { "from": "result.route" }
},
"function": { "name": "assignToSales" }
}function.name is a valid identifier: letters, digits, _ and $, up to 64 characters, not starting with a digit. The editor prints the handler you need to register.
In the response
{ "key": "assign_owner", "type": "function", "function": "assignToSales", "params": { "email": "ana@acme.com", "route": "sales" } }A function entry has no status when it fired. When a required param is missing or the limit is reached, the entry is "status": "skipped" instead — and must not be called.
With the SDKs
Pass your handlers to decide: the SDK calls them after the decision, with the params and the decision itself.
import { Dcision } from "@dcision/sdk";
const dcision = new Dcision(); // reads DCISION_API_KEY
const decision = await dcision.decide("lead-qualification", { message, email, company_size: 500 }, {
idempotencyKey: `lead-${lead.id}`,
functions: {
assignToSales: async (params, decision) => crm.assign(String(params.email), decision.execution_id),
},
});
decision.functionResults; // [{ key: "assign_owner", function: "assignToSales", result: … }]How the SDKs dispatch:
- Handlers run in the order of
destinations, one at a time: an async handler is awaited before the next starts. Only function entries without astatusare called. - Every handler is looked up first. If one is missing, the call fails with
FUNCTION_NOT_REGISTEREDand no handler runs — or passonMissingFunction: "ignore"(on_missing_function="ignore"in Python) to skip it. - A handler that throws stops the dispatch: the call fails with
FUNCTION_FAILED, carrying the error ascause, thedecisionand the results of the handlers that completed. The decision already ran — and was billed. - The Python client is synchronous: an
async defhandler fails withFUNCTION_FAILED.
Dispatch later
Dispatch somewhere else — a worker, a queue consumer — from the stored response:
import { Decision } from "@dcision/sdk";
const decision = await dcision.decide("lead-qualification", state); // no functions: nothing is called
await queue.push(JSON.stringify(decision));
// in the worker
const results = await new Decision(JSON.parse(job.payload)).dispatch(handlers, { onMissingFunction: "ignore" });Make handlers idempotent
The SDKs dispatch once per decide call, after the final answer — their internal retries never dispatch twice. But calling decide again with the same Idempotency-Key returns the stored answer, and its functions are dispatched again. Key the work on decision.execution_id, or on your own record ID, so a second call is harmless.
Without an SDK
Loop over the entries yourself:
for (const destination of decision.destinations ?? []) {
if (destination.type !== "function" || destination.status) continue; // skipped entries carry a status
await handlers[destination.function](destination.params, decision);
}HTTP requests
Call any API after a decision — method, URL, headers and a JSON body built from variables and workspace secrets — delivered with retries, an Idempotency-Key and a signature.
Workspace secrets
Keep tokens and secret URLs out of decisions — workspace secrets referenced as {{secrets.NAME}}, who can manage them, how they are stored and masked, and when changes apply.