Functions

Call built-in and custom @ functions, and define your own with defineFunction.

Functions compute values inside a program. The model writes the call, and the renderer runs the code on the client. The numbers come from your data, not from the model.

For example, a ticket dashboard shows "Open tickets: 12". The model writes @Count(@Filter(tickets.rows, "status", "==", "open")). The renderer counts the rows when the query returns and counts again when the rows change. The model never sees the rows.

Function calls start with @. The @ marks a call (function or action), not a component. OpenUI Lang has built-in functions. When they are not enough, such as for currency formatting or a calculation specific to your domain, a library adds its own (custom functions).

Calling a function

openTickets = @Filter(tickets.rows, "status", "==", "open")

Arguments are positional, the same rule as component props. null in place of an optional argument skips it. Calls can be nested:

openCount = @Count(@Filter(tickets.rows, "status", "==", "open"))

The @ is required. Count(x) is not a function call.

Invalid arguments

Built-ins convert their arguments instead of validating them. @Sum(["1", 2]) gives 3.

Custom function arguments go through the same checks as component props, so a mistake in one call does not break the page.

Literal arguments are checked when the program is parsed:

ProblemError codeResult
Wrong type for a required paramtype-mismatchThe call evaluates to null.
Required param missing or nullmissing-required or null-requiredThe call evaluates to null.
Wrong type for an optional paramtype-mismatchThe argument is dropped and the default applies.
More arguments than paramsexcess-argsThe extra arguments are dropped. The call runs.

Arguments that are only known at runtime (a $variable, a reference, a query result, an expression) are checked when the call runs:

  • A required argument that is null or missing, with no default, makes the call evaluate to null with no error. This covers an unset $variable or a query that has not returned yet.
  • A required argument of the wrong type reports runtime-error, and the call evaluates to null.
  • An optional argument of the wrong type reports runtime-error, and its default applies. The call runs.
  • If the function throws, the call reports runtime-error (@Percent threw: ...) and evaluates to null.
  • If the result does not match its returns schema, the call reports runtime-error and evaluates to null. Without returns, the result is used as is.

The React Renderer sends these errors to onError once the stream has ended.

Custom actions are stricter. Any invalid argument there, optional or not, makes the action a no-op.

Unknown functions

A call to an @Name that is not a built-in function, an action, or a library function reports unknown-function and evaluates to null. The rest of the statement still renders:

root = TextContent("Total: " + @Total(items))

If the library has no Total function, this renders "Total: ".

Built-in functions

The system prompt lists the built-ins when toolCalls or bindings is enabled, or when you set the builtinFunctions prompt option. They are mostly used to filter, sort, and aggregate Query results.

Aggregation

FunctionWhat it doesExample
@Count(array)Length of array@Count(tickets.rows) → 42
@Sum(array)Sum of numbers@Sum(data.rows.amount) → 1250
@Avg(array)Average@Avg(data.rows.score) → 4.2
@Min(array)Smallest value@Min(data.rows.price) → 9.99
@Max(array)Largest value@Max(data.rows.price) → 99.99
@First(array)First element@First(data.rows)
@Last(array)Last element@Last(data.rows)

Filtering & Sorting

FunctionWhat it does
@Filter(array, field, op, value)Keep items where field matches. Ops: ==, !=, >, <, >=, <=, contains
@Sort(array, field, direction?)Sort by field. Direction: "asc" (default) or "desc"
sorted = @Sort(tickets.rows, "created", "desc")

Nesting @Count and @Filter is the main pattern for KPI cards:

kpi = Card([
  TextContent("Open Tickets", "small"),
  TextContent("" + @Count(@Filter(data.rows, "status", "==", "open")), "large-heavy")
])

Math

FunctionWhat it does
@Round(number, decimals?)Round to N decimal places
@Abs(number)Absolute value
@Floor(number)Round down
@Ceil(number)Round up

Iteration with @Each

Render a template for every item in an array:

priorityTags = @Each(tickets.rows, "t", Tag(t.priority, null, "sm"))

The second argument ("t") is the loop variable name. Use it inside the template.

Actions are not functions

@Run, @Set, @Reset, @ToAssistant, and @OpenUrl are actions, not functions. A function computes a value for a prop. An action runs when the user clicks. See Actions.

Built-in signatures

The built-in functions are defined with defineFunction inside @openuidev/lang-core, so their prompt lines are generated the same way as custom ones:

@Sum(numbers: number[]) → number
@Round(number: number, decimals?: number) → number
@Filter(array: any[], field: string, operator: "==" | "!=" | ">" | "<" | ">=" | "<=" | "contains", value: any) → any[]

Custom functions

A library can add its own functions next to the built-ins. The model calls them with an @ prefix, like @Count, and the renderer runs your implementation on the client. Use them for what the built-ins do not cover, such as formatting and small domain calculations that the model should not do by hand.

Define a function

import { createLibrary, defineComponent, defineFunction } from "@openuidev/react-lang";
import { z } from "zod/v4";

const percent = defineFunction({
  name: "Percent",
  description: "Formats part / total as a percentage",
  params: z.object({
    part: z.number(),
    total: z.number().default(100),
  }),
  returns: z.string(),
  fn: ({ part, total }) => `${((part / total) * 100).toFixed(1)}%`,
});

const TextContent = defineComponent({
  name: "TextContent",
  description: "A line of text",
  props: z.object({ text: z.string() }),
  component: ({ props }) => <p>{props.text}</p>,
});

export const library = createLibrary({
  root: "TextContent",
  components: [TextContent],
  functions: [percent],
});

The model can now write:

root = TextContent("Done: " + @Percent(42, 120))

This renders "Done: 35.0%".

To add a function to an existing library, such as openuiLibrary from @openuidev/react-ui, use library.extend.

Fields

FieldDescription
nameThe name after @ in programs. By convention, start it with an uppercase letter.
descriptionShown next to the signature in the prompt.
paramsA z.object(...). Key order is the positional order of the arguments.
returnsOptional. A Zod schema for the result. It is printed in the prompt and checked at runtime.
fnThe implementation. It receives one object keyed by the params names, with defaults applied.

params must be a z.object(...). Anything else throws when you call defineFunction.

What fn receives

Arguments map to params in key order. fn receives them as one object:

Callfn receives
@Percent(42, 120){ part: 42, total: 120 }
@Percent(42){ part: 42, total: 100 }
@Percent(42, null){ part: 42, total: 100 }

A missing optional argument, or null in its place, takes the param's default. An optional param with no default is left out of the object. Keep fn pure and synchronous; it can run many times.

Using functions in your app

Pass the library to the Renderer from @openuidev/react-lang, or to AgentInterface. The renderer runs fn and sends errors to onError:

<Renderer library={library} response={response} onError={(errors) => console.warn(errors)} />

The Vue, Svelte, and Angular renderers are on 0.4 and do not run custom functions yet. Parsers built from library.toJSONSchema(), including Autofix in @openuidev/server, accept the calls.

Complete example

A sales summary where the renderer computes the percentages.

Your app adds the function to openuiLibrary and renders with the extended library:

"use client";

import { defineFunction, Renderer } from "@openuidev/react-lang";
import { openuiLibrary } from "@openuidev/react-ui";
import { z } from "zod/v4";

const percent = defineFunction({
  name: "Percent",
  description: "Formats part / total as a percentage",
  params: z.object({ part: z.number(), total: z.number().default(100) }),
  returns: z.string(),
  fn: ({ part, total }) => `${((part / total) * 100).toFixed(1)}%`,
});

// Create the library once, at module scope.
const library = openuiLibrary.extend({ functions: { add: [percent] } });

export function Summary({ response }: { response: string }) {
  return <Renderer library={library} response={response} />;
}

The model writes:

root = Stack([header, tbl, summary])
header = CardHeader("Q3 sales")
regions = ["North", "South", "West"]
sales = [42, 75, 120]
targets = [120, 100, 100]
tbl = Table([Col("Region", regions), Col("Sales", sales, "number"), Col("Target", targets, "number")])
summary = TextContent("North reached " + @Percent(42, 120) + " of its target. The team reached " + @Percent(@Sum(sales), @Sum(targets)) + ".")

The user sees a table with three regions and the line "North reached 35.0% of its target. The team reached 74.1%." The renderer computed both numbers. The model only wrote the calls.

When to use it

Use functions when:

  • A value is derived from data: counts, sums, averages, or a filtered list.
  • The value depends on Query results or $variables, so it must update without the model.
  • Your app formats values in a fixed way, such as currency or percentages. Add a custom function for it.

Do not use them when:

  • The function would have side effects, such as writing data or opening a link. Use an action instead.
  • The work is slow or async. fn must be pure and synchronous. Use a Query for data from your backend.

On this page