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

# Custom code: lambdas, microservices and HTTP calls

> What to reach for when the typed surface cannot express something: native filters first, then a JavaScript lambda, an HTTP call to an existing service, or a microservice container.

Most backend logic fits the typed statements, filters and expressions. When it does not, pick
the lightest option that does the job, in this order:

| You need | Use |
| - | - |
| Arithmetic, string, list or date work | Native filters and expressions (`fl.*`, `expr`, `obj`) |
| A short JavaScript body | A lambda: `s.lambda`, or a filter such as `fl.map` |
| A service that already has an HTTP API | `s.api.request` |
| A long-running process, or code in another language | A `microservice()` container, called with `s.microservice.request` |

## Native filters first

A filter chain runs inside the request, with no extra process. This adds 20% and rounds to two
places:

```ts theme={null}
// xano/custom/native.ts
import { defineFunction, input, inp, ref, s, withFilters, fl } from "@xano/sdk";

export const withTax = defineFunction({
  name: "with_tax",
  input: { subtotal: input.decimal({ required: true }) },
  stack: [s.set_var("total", withFilters(inp("subtotal"), fl.mul(1.2), fl.round(2)))],
  response: ref("total"),
});
```

Given `120.5`, it returns `144.6`. Every filter and its arguments are in the
[authoring reference](/reference/authoring).

## A lambda, for a short JavaScript body

`s.lambda` runs a JavaScript body as a statement and binds what it returns. Write the body as a
function: its parameters are the bindings, so a wrong name is a compile error. A stack variable
is `$var.name`; `$input`, `$env` and `$auth` are there too.

```ts theme={null}
// xano/custom/lambda.ts
import { defineFunction, input, inp, ref, s, c, withFilters, fl } from "@xano/sdk";

export const orderTotal = defineFunction({
  name: "order_total",
  input: { lines: input.json({ required: true }) }, // [{ qty, price }, ...]
  stack: [
    s.set_var("lines", inp("lines")),
    s.lambda({
      as: "total",
      // the body is sent as text; bindings arrive as parameters
      code: ({ $var }) =>
        $var.lines.reduce((sum: number, l: { qty: number; price: number }) => sum + l.qty * l.price, 0),
    }),
  ],
  response: ref("total"),
});

export const doubled = defineFunction({
  name: "doubled",
  stack: [
    s.set_var("nums", c.array([1, 2, 3])),
    s.set_var("out", withFilters(ref("nums"), fl.map(({ $this }) => $this * 2))), // [2, 4, 6]
  ],
  response: ref("out"),
});
```

The filters `fl.map`, `fl.filter`, `fl.reduce`, `fl.find` and their relatives also take a body,
with `$this` (the element), `$index` and `$parent`. A filter sends its body once for the whole
list, which is cheaper than calling a lambda from inside a loop.

* **Only the body crosses.** A `const` from the surrounding module is undefined inside it. Pass
  values in with `lam.fn(fn, { capture: { ... } })`.
* **A throw does not fail the request.** The error text comes back as the value with HTTP 200,
  so check a lambda's result before you rely on it.
* **No imports.** Libraries such as `_`, `axios`, `jose`, `luxon` and `uuid` are preloaded
  globals, along with `fetch` and `crypto`.

### A body in its own file

Once a body has real structure, put it in a module under `xano/lambdas/` and read it with
`lam.file`. The module default-exports one function; its own tsconfig type-checks it, and the
file is read as text at build time. `lam.file` reads the filesystem, so it comes from
`@xano/sdk/node`:

```ts theme={null}
// xano/lambdas/order-total.ts
import type { LambdaBindings } from "@xano/sdk";

export default ({ $result, $this }: LambdaBindings<"reduce">) => {
  const line = $this.qty * $this.price;
  const discount = $this.qty >= 10 ? 0.1 : 0;
  return $result + line * (1 - discount);
};
```

```ts theme={null}
// xano/custom/lambda-file.ts
import { defineFunction, input, inp, ref, s, withFilters, fl } from "@xano/sdk";
import { lam } from "@xano/sdk/node";

export const bulkTotal = defineFunction({
  name: "bulk_total",
  input: { lines: input.json({ required: true }) },
  stack: [
    s.set_var(
      "total",
      withFilters(inp("lines"), fl.reduce({ initial_value: 0, code: lam.file("../lambdas/order-total.ts") })),
    ),
  ],
  response: ref("total"),
});
```

Given `[{ qty: 2, price: 10 }, { qty: 12, price: 5 }]`, it returns `74`. Move any helper inside
the default export: only its body is sent.

## An HTTP call, for a service with an API

`s.api.request` calls any HTTP API and binds a `{ request, response }` envelope. `params` is the
query string for `GET` and the body otherwise. Keep credentials in a header read from `env()`:

```ts theme={null}
// xano/custom/http.ts
import { defineFunction, input, inp, ref, s, c, expr, env } from "@xano/sdk";

export const lookupCompany = defineFunction({
  name: "lookup_company",
  input: { domain: input.text({ required: true }) },
  stack: [
    s.api.request({
      as: "company",
      method: "GET",
      url: "https://api.example.com/v1/companies",
      params: { domain: inp("domain") },
      headers: {
        "User-Agent": "my-app",
        Authorization: env("COMPANY_API_KEY"),
      },
      timeout: 10,
    }),
    s.precondition({
      expr: expr(ref("company.response.status"), "=", c.int(200)),
      error: c.text("The company lookup failed."),
      error_type: "badrequest",
    }),
  ],
  response: ref("company.response.result"), // never return the whole envelope: it echoes your headers
});
```

* **A failed call does not throw.** An error status is an ordinary response, and a timeout or
  DNS failure is `response.status` `0`. Check the status, as the precondition does here.
* **Send a `User-Agent`.** None is sent by default, and some APIs answer 403 without one.
* **A structured body** goes in `obj({ ... })`, which carries values from the stack at any depth.

Declare `COMPANY_API_KEY` in `workspaceConfig({ env })` and keep its value in `xano/.env` (see
[Env and secrets](/build/env-and-secrets)).

## A microservice, for a container

A `microservice()` is a container workload deployed alongside the workspace: a long-running
process, or code in a language other than JavaScript. A stack calls it with
`s.microservice.request`:

```ts theme={null}
// xano/custom/microservice.ts
import { microservice, defineFunction, ref, s, c } from "@xano/sdk";

export const pdfRenderer = microservice({
  name: "pdf_renderer",
  deployment: {
    replicas: 1,
    containers: [
      {
        name: "renderer",
        image: "ealen/echo-server:latest",
        ports: [{ servicePort: "8080", containerPort: "80" }],
        resources: { cpu: "250m", ram: "512Mi" },
        env: [
          { name: "MODE", value: "production" },
          { name: "LICENSE_KEY", fromEnv: "PDF_LICENSE_KEY" }, // only the name travels in the bundle
        ],
      },
    ],
  },
});

export const renderPdf = defineFunction({
  name: "render_pdf",
  stack: [
    s.microservice.request({
      as: "rendered",
      host: pdfRenderer, // binds by name; the one servicePort resolves on its own
      path: c.text("/render"),
      method: "POST",
      params: c.obj({ template: "invoice" }),
    }),
  ],
  response: ref("rendered.response.result"),
});
```

* **Pass the def as `host`.** A rename then fixes every call site, and the port is checked
  before deploy. A microservice with several `servicePort`s needs `port` on the call.
* **Secrets.** A container's `env` entry with `fromEnv` names a `workspaceConfig({ env })`
  variable, and Xano fills in the value at deploy. Never write a secret as a literal `value`, and
  do not use `process.env.X` in the def: it is read at export and the value lands in the bundle.
* **A Helm chart** is the other shape: `kind: "helm"` with a `chart` and its `values`. Use
  `${env.NAME}` for a secret inside `values`.
* **Private registries.** `registryAuth.dockerconfigjson` is stored as written, so a bundle that
  carries one is secret material. Prefer a public image.

The microservice surface is new and may change between releases; the SDK prints a notice on
every export that includes one.

## Register them

```ts theme={null}
export default workspace("my-app")
  // ...tables, API groups, queries
  .registerFunctions([withTax, orderTotal, doubled, bulkTotal, lookupCompany, renderPdf])
  .registerMicroservices([pdfRenderer]);
```

## Next

* Lambda bindings by surface, `lam.fn` capture, and every `s.api.request` field:
  [authoring reference](/reference/authoring).
* The `microservice()` def's full shape: [object kinds reference](/reference/object-kinds).
* Test these functions where they are defined: [Tests](/build/tests).


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