> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trycurious.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Decide with the server SDK

> decide() and decideMany(): the required fallback, the deadline, the result, the camelCase declaration, idempotency keys, and the setting that makes it throw.

```ts theme={null}
const result = await curious.decide({
  element: "welcome_email_subject",
  userId: user.id,
  fallback: { subject: "Welcome to Acme" },
  identifiers: { email: user.email },
  attributes: { plan: user.plan },
  context: { locale: "en-US" },
  idempotencyKey: jobId,            // optional; makes a retry of this call safe
  declaration: {                    // optional; the secret-key declaration, camelCase
    elementType: "email_subject",
    control: { subject: "Welcome to Acme" },
    rewards: [{ name: "email_clicked", windowSeconds: 86_400 }],
    options: { assignment: "static" },
  },
});

sendEmail(result.content.subject);
```

## The fallback is required

`fallback` is the content you would show without Curious: your control. It serves when the deadline (`decideTimeoutMs`, 1.5 seconds by default) passes, when the request fails with any status, and when the response's content does not have the fallback's shape (reported as `content_shape_mismatch`). It also types the result: `result.content` has the fallback's type, so `result.content.subject` is a string whether Curious or the fallback served.

`decide()` never throws by default and never retries. A retry would create a second decision; if you must retry, pass the same `idempotencyKey` and the retry replays the original.

## The result

```ts theme={null}
type DecisionResult<T> = {
  content: T;
  served: "curious" | "fallback";
  armId: string | null;
  armType: "control" | "variant" | null;
  decisionId: string | null;
  experimentId: string | null;
  experimentStatus: "initializing" | "exploring" | "concluded" | null;
  replayed: boolean;
  warnings?: { code: string; message: string }[];
  error?: CuriousErrorSummary;   // why the fallback served
  requestId: string | null;
  durationMs: number;
};
```

When `served` is `"fallback"`, `error` says why (`timeout`, `network_error`, `server_error`, `undeclared_element`, `content_shape_mismatch`, …) and the id fields are `null`. Log `error.code` and `requestId`; see [Fallbacks and timeouts](https://docs.trycurious.ai/guides/fallbacks.md).

## The declaration

`declaration` carries the secret-key declaration fields in camelCase, converted to the wire names for you: `elementType`, `control`, `arms`, `rewards` (with `windowSeconds`, `baselineRate`), `constraints` (with `thresholdType`, `windowSeconds`), `requirements` (`forbiddenPhrases`, `requiredPhrases`), `instructions`, `format`, `variables`, `parameters`, and `options` (`assignment`, `approval`, `holdout`, `minimumEffectSize`). `format`, `content` and `parameters` pass through in their wire shape. Send the same declaration on every call, or move it to [`declare()`](https://docs.trycurious.ai/sdks/node/events#declare); see [Declarations](https://docs.trycurious.ai/concepts/declarations.md).

## Several elements at once

```ts theme={null}
const results = await curious.decideMany({
  userId: user.id,
  context: { page: "/pricing" },
  elements: [
    { element: "pricing_hero", fallback: "Ship better experiments" },
    { element: "pricing_cta", fallback: "Start free" },
  ],
});
```

Sends the batch form and returns one result per element, in order. A failing item serves its own fallback with its own `error`; the envelope-level failure, such as a timeout, serves every fallback. See [Batch calls](https://docs.trycurious.ai/guides/batch.md).

## Making it throw

`throwOnError: true` on the client makes `decide()` throw a `CuriousError` instead of serving the fallback. It exists for scripts and tests that want a loud failure. Never use it in a delivery path.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.