---
title: "Create a Function"
sidebarTitle: "Create Function"
description: "Register Python as a column: CPU, GPU, and remote-service Functions, called once per row or once per batch."
icon: code
keywords: ["function", "udf", "cpu", "gpu", "batch", "scalar", "blob", "remote api", "embedding", "rate limiter"]
availability:
embedded: unavailable
enterprise: available
---
Some curation steps cannot be written as a SQL expression: an embedding
model, an image decode, a call to an external service. A Function is a
Python callable registered with the database, versioned, and then applied as
a column or called in a query -- the code runs where the data is, not pulled
into your process first. This page covers how to shape one; later curation
tasks such as [Chunking](/curation/chunking) assume you have read it.
## Scalar: once per row {#scalar}
The plain form takes one row's values and returns one result. Types are
inferred from the annotations:
```python Python icon="python"
from lancedb import udf
@udf(pip=["numpy==2.2.0"])
def score(value: float) -> float:
return value * 2
version = db.create_function_async(score).wait()
table.add_columns({"doubled": version(value=col("value"))})
```
`pip` declares the worker's dependencies; they are part of the function's
immutable version, so changing one produces a new version rather than
altering rows a previous version already filled.
## Batch: once per call, many rows at a time {#batch}
A function is called once per batch instead of once per row when its
parameters are annotated `pyarrow.Array`, or when it takes a single
`pyarrow.RecordBatch` parameter to read more than one column together. The
same doubling logic, vectorized over a whole batch at once:
```python Python icon="python"
import pyarrow as pa
import pyarrow.compute as pc
from lancedb import udf
@udf(pip=["numpy==2.2.0"])
def score_batch(value: pa.Array) -> pa.Array:
return pc.multiply(value, 2)
```
Batching amortizes per-call overhead across many rows, which matters most
once a function also does something with real fixed cost per call -- loading
a model, opening a connection, making a network round trip.
## Stateful Functions: load once, call many times {#stateful}
A `@udf`-decorated **class** is the same callable with state. Each remote
instance calls `__init__` once, calls `__call__` for every row or batch it
processes, and calls `close()`, if defined, once when it retires:
```python Python icon="python"
from lancedb import udf
@udf
class scale:
def __init__(self, factor: float, offset: float = 0.0):
self.factor = factor
self.offset = offset
def __call__(self, value: float) -> float:
return value * self.factor + self.offset
```
Constructor parameters are **initialization fields**, bound once per binding
rather than read per row -- mixed with per-row columns in the same call:
`version(value=col("value"), factor=2.0)`. Load models, open clients, and
build rate limiters in `__init__`; every batch the instance serves then
reuses them, since an instance's state is shared across its own batches but
never across other instances or workers. This is the pattern GPU inference
and remote-API Functions both build on below.
## GPU: batch inference, loaded once {#gpu}
`gpu=True` requires a GPU-capable worker for every execution. Combined with
the batch and stateful forms above, a realistic embedding model loads once
in `__init__` and runs one forward pass per batch, not one per row:
```python Python icon="python"
import pyarrow as pa
from lancedb import udf
@udf(gpu=True, pip=["torch==2.12.0", "sentence-transformers==5.7.0"])
class embed_text:
def __init__(self, model_name: str = "all-MiniLM-L6-v2"):
import torch
from sentence_transformers import SentenceTransformer
device = "cuda" if torch.cuda.is_available() else "cpu"
self.model = SentenceTransformer(model_name, device=device)
def __call__(self, text: pa.Array) -> pa.Array:
texts = [t or "" for t in text.to_pylist()]
embeddings = self.model.encode(texts)
return pa.array(embeddings.tolist(), type=pa.list_(pa.float32(), list_size=384))
version = db.create_function_async(embed_text).wait()
table.add_columns({"embedding": version(text=col("caption"))})
```
The model loads once per worker instance, in `__init__`; `__call__` only
encodes the batch it was handed. Registering the same logic as a plain
function instead would reload the model on every single row.
## Calling a remote service {#remote}
A function that calls an external API needs a credential without putting it
in the function's source. A Secret is a database-scoped named credential:
`db.create_secret` stores the value once, and binding it at registration time
puts it in the environment variable the function reads, without ever
returning it from any API or recording it in the function's own definition.
Combine it with the stateful form so the HTTP client -- and any rate limiter
around it -- is built once, not reopened on every row:
```python Python icon="python"
import os
from lancedb import EnvVarSecret, udf
db.create_secret("openai-prod", os.environ["OPENAI_API_KEY"])
@udf
class embed_remote:
def __init__(self, model: str = "text-embedding-3-small"):
import os
from openai import OpenAI
self.client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
self.model = model
def __call__(self, text: str) -> list[float]:
response = self.client.embeddings.create(input=text, model=self.model)
return response.data[0].embedding
version = db.create_function_async(
embed_remote,
secrets=[EnvVarSecret(secret_name="openai-prod", env_variable="OPENAI_API_KEY")],
).wait()
```
A mistyped Secret name is caught at registration; a mistyped environment
variable name only surfaces on the first rows, since nothing records which
variables a function reads. There is no built-in retry or per-row
error-skip policy -- a function that calls a flaky service should handle
timeouts and retries itself, inside `__call__`.
## Blob v2 input and output {#blob}
A Function can bind a [Blob v2 column](/management/table#managed-blobs) and
return another one, including a blob nested inside a struct field, not only
at the top level. In row mode, a blob parameter arrives as Python `bytes`
(or a `BlobReader` for incremental, seekable access, so a function never
has to materialize a payload it only partially needs):
```python Python icon="python"
import pyarrow as pa
import lancedb
from lancedb import BlobReader, udf
@udf(
gpu=True,
pip=["av==13.1.0", "pillow==11.0.0"],
input_schema=pa.schema([
lancedb.blob("video"),
pa.field("ts", pa.float64(), nullable=False),
]),
output_schema=lancedb.blob("jpeg", nullable=False),
)
def extract_frame(video: BlobReader, ts: float) -> bytes:
... # decode the frame at ts, encode as JPEG, return its bytes
```
`output_schema=lancedb.blob(...)` is what makes the result Blob v2 rather
than plain Arrow `binary` -- returning `bytes` with no explicit blob output
schema still holds the right bytes, but skips the Managed storage path and
`fetch_blobs`. Declare it explicitly on every function whose output should
be a blob.
## What to read next {#next}
A Function whose output is a list, exploded into one row per element.
Filling the column a Function is bound to, and re-filling it as inputs change.