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 a status are called.
  • Every handler is looked up first. If one is missing, the call fails with FUNCTION_NOT_REGISTERED and no handler runs — or pass onMissingFunction: "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 as cause, the decision and the results of the handlers that completed. The decision already ran — and was billed.
  • The Python client is synchronous: an async def handler fails with FUNCTION_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);
}

On this page