Skip to content

Particles

A Particle is a function you write — in ordinary Python or Rust — that ParticleDB registers as a first-class SQL function. Once it is loaded, you call it inline in a query exactly like a built-in:

SELECT name, haversine_km(lat, lon, 37.77, -122.42) AS km_from_sf
FROM stores
ORDER BY km_from_sf;

Think of Particles as custom keywords for SQL. SQL gives you SUM, UPPER, NOW() — but the moment you need something it doesn’t have, you normally give up on the database: you pull the rows into Python/pandas, a Rust service, or your application, compute there, and push results back. Particles delete that round trip. You write the function once, and it runs inside the query — row by row, or group by group.

These are things people constantly re-implement in another language because standard SQL has no keyword for them. As Particles they become one-liners you can call from any query, view, or GROUP BY:

You want…SQL has no…As a Particle
The median of a groupplain median() (only the verbose percentile_cont)pmedian(amount)
Great-circle distance between two lat/lon pointsany geo-distance functionhaversine_km(lat1, lon1, lat2, lon2)
A per-row z-score within a windowzscore(...) OVER (...)zscore(x) OVER (PARTITION BY g)
A URL slug from a titleslugify()slugify(title)
A SHA-256 / HMAC hex digestcrypto hashing built insha256_hex(payload)

Every one of those is a real, tested example used throughout this guide — nothing here is hypothetical.

Particles come in two flavours, depending on whether the work fits inside a query or has to run beyond it.

These execute as part of query evaluation and return before the query does — one value per row, or per group:

KindSQL conceptStatus
@scalara per-row functionAvailable
@aggregatea group fold (GROUP BY)Available
@windowa window function (OVER (...))Available
@casta user-defined cast between typesAvailable
@typea user-defined typeReserved — coming

@scalar, @aggregate, @window, and @cast run today — all covered in Python. @type is declared surface with no runtime yet — the decorator exists so your modules stay forward-compatible, but a @type Particle is not executed.

Some work is not a per-row function: calling an LLM, running an agent, or driving a multi-step process takes seconds to minutes and must survive restarts. Those are asynchronous Particles:

KindWhat it isStatus
@agentone bounded AI/external step — an LLM call, an API call, a tool usePreview
@workflowa durable, multi-step agentic process (a DAG of steps)Preview

This is the AI-native part of ParticleDB — see below.

The synchronous Particles above can be authored in either Python or Rust — choose per Particle (the asynchronous @agent / @workflow tier is Python):

  • Python — the fast path to write. A normal function with type hints, deployed with one command. Best for most functions, quick iteration, and anything that leans on the Python ecosystem.
  • Rust — the fast path to run. Compiled, batch-at-a-time, zero-copy over Apache Arrow. Best for performance-critical Particles on a tight loop. (Preview tier.)

The concept is identical in both: your function’s parameter and return types become the SQL signature. Here is the same idea in each language.

geo.py
from particledb import scalar
@scalar
def haversine_km(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
"""Great-circle distance between two lat/lon points, in kilometres."""
from math import radians, sin, cos, asin, sqrt
r = 6371.0088
dlat = radians(lat2 - lat1)
dlon = radians(lon2 - lon1)
a = sin(dlat / 2) ** 2 + cos(radians(lat1)) * cos(radians(lat2)) * sin(dlon / 2) ** 2
return 2 * r * asin(sqrt(a))
SELECT haversine_km(40.71, -74.01, 37.77, -122.42); -- 4129.06 (NYC → SF, km)

float -> float becomes double precision -> double precision. Load the module and it’s a SQL function. → Full Python guide

This is what makes ParticleDB AI-native: the database doesn’t just store the data your AI app reads — it can run the AI step itself, durably, right where the data lives. No separate queue, worker fleet, or orchestration service to stand up.

Author an agent as an ordinary function:

triage.py
from particledb import agent
@agent
def triage_ticket(subject: str, body: str) -> str:
"""One bounded AI step: classify a support ticket with your model."""
# call your LLM / tool here — the engine runs this durably, off the query path
...
return category

Unlike a scalar, an agent is submitted, not evaluated inline — it returns a run_id immediately and runs in the background: retry-safe, idempotent, and deadline-bounded, backed by a durable-execution engine.

-- Kick off ONE run — returns a run_id right away, does NOT block the query:
SUBMIT PARTICLE triage_ticket(subject => 'refund?', body => '');
-- run_id: 9b1f…
-- …then track it, by run_id, whenever you like:
SELECT particle_run_status('9b1f…'); -- RUNNING | COMPLETED | FAILED | …
SELECT particle_run_result('9b1f…'); -- the answer, once COMPLETED

A workflow is the same idea for a durable multi-step process — a DAG of steps you write in Python. Both kinds — plus the two things that matter, how you invoke them and how you track the async run — are covered in Async Agents & Workflows.

  1. Write your function in Python or Rust — the type signature is the SQL signature.
  2. Load it into a running server — one flag at startup or one deploy command. → Loading
  3. Call it from SQL — it’s now a first-class function, everywhere a built-in works.