Skip to main content
Version: 0.3.36

Creating Functions

A function is server-side code that RaisinDB runs on demand: when you call it over HTTP or SQL, when a trigger fires, or when an AI agent uses it as a tool. It runs in a sandbox with a raisin API for reading and writing nodes, running SQL, calling external services and sending email.

Runtimes

Runtimelanguage--langBuild step
QuickJS (JavaScript)javascriptjsnone, the source ships
Starlark (Python-like)starlarkstarlarknone, the source ships
WebAssemblywasmrust, go, assemblyscript, tscompiled to a component

JavaScript is the quickest to iterate on. WebAssembly is the choice for CPU-bound work or when you want to reuse Rust or Go libraries. Starlark suits small, deterministic data transformations.

What a function is made of

A function is a raisin:Function node in the functions workspace, with its code stored in a child asset node. Two properties tie them together:

  • language selects the runtime.

  • entry_file is <file>:<handler>: the asset beside the node and the function inside it to call. The default is index.js:handler.

    The handler half is optional. A bare file name takes the language's default handler, which is handler for JavaScript and Starlark and default for WebAssembly, so main.wasm and main.wasm:default mean the same thing. A bare name with no extension is read the old way, as a handler inside index.js.

# content/functions/lib/docs/greet/.node.yaml
node_type: raisin:Function
properties:
title: greet
name: greet # used in URLs and SQL calls
language: javascript
entry_file: index.js:handler
execution_mode: both # async (default), sync, or both
enabled: true
resource_limits:
timeout_ms: 30000
max_memory_bytes: 134217728
network_policy:
http_enabled: false
// content/functions/lib/docs/greet/index.js
export function handler(input) {
console.log(`greeting ${input.name}`);
const children = raisin.nodes.getChildren('content', '/pages', 50);
return { greeting: `Hello, ${input.name}`, pages: children.length };
}

execution_mode decides how the function may be called. async (the default) runs it as a background job. sync runs it inline and returns the result in the response. both allows either.

By convention application functions live under /lib/<namespace>/<name>, and the built-in ones under /lib/raisin/....

Create a function with the CLI

Inside a package directory (one that has a manifest.yaml and a content/ folder), scaffold the node and its source in one step:

raisindb create function greet --lang js --ns docs --description "Greets by name"

This writes the two files shown above under content/functions/lib/docs/greet/. Use --lang starlark for a main.star handler instead, or one of the WebAssembly languages (see WebAssembly functions for the project layout they add).

Deploy the package and install it into a repository:

raisindb deploy . --repo myapp --install

Every function under content/functions/ in the package becomes a node in the repository's functions workspace.

Create a function over HTTP

The same two nodes can be created with the repository API. First the function node under /lib, then its code as a child asset with an inline code property:

curl -X POST http://localhost:8090/api/repository/myapp/main/head/functions/lib \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"node":{"name":"greet","node_type":"raisin:Function","properties":{
"title":"Greet","name":"greet","language":"javascript",
"entry_file":"index.js:handler","execution_mode":"both","enabled":true}}}'

curl -X POST http://localhost:8090/api/repository/myapp/main/head/functions/lib/greet \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"node":{"name":"index.js","node_type":"raisin:Asset","properties":{
"title":"index.js","file":"",
"code":"export function handler(input) { return { greeting: \"Hello, \" + input.name }; }"}}}'

Function code

The handler receives the caller's input as a plain object and returns a JSON-serialisable value. In JavaScript the raisin.* calls are synchronous: you can write await in front of them, but you do not need to.

export function handler(input) {
// Read a node (workspace, path)
const page = raisin.nodes.get('content', input.path);

// Merge properties into it; keys you do not name are kept
raisin.nodes.update('content', input.path, {
properties: { status: 'processed' },
});

// SQL with bound parameters; query returns an array of row objects
const rows = raisin.sql.query(
"SELECT path, name FROM 'content' WHERE node_type = $1",
['blog:Article']
);

// Outbound HTTP, allowed by the function's network_policy
const res = raisin.http.fetch('https://api.example.com/notify', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: { page: page.id },
});

return { ok: res.status === 200, count: rows.length };
}

Every function sees the same API. The main namespaces:

NamespaceMethods
raisin.nodesget, getById, getChildren, query, create, createDeep, upsertDeep, update, updateProperty, delete, move, history, beginTransaction
raisin.sqlquery, execute
raisin.httpfetch(url, options), plus request(method, url, options) and get / post / put / patch / delete; the global fetch() is also available in JavaScript
raisin.eventsemit(type, data)
raisin.secrets, raisin.emailread vaulted secrets, send email
raisin.locks, raisin.inventorylease locks and counting reservations
raisin.imap, raisin.ai, raisin.assets, raisin.functionsIMAP, model calls, asset processing, calling other functions
raisin.context{ tenant_id, repo_id, branch, workspace_id, actor, execution_id }

Outbound HTTP is off until the node declares a network_policy with http_enabled: true and an allowed_urls list of glob patterns (* matches within one path segment, ** across segments). A request to a URL outside the list comes back with status: 0 and an error message rather than leaving the server. Email and secrets are gated the same way by email_policy and secret_policy.

In JavaScript, most raisin.* calls report failure through their return value instead of throwing: sql.query returns { error, rows: [] }, sql.execute returns -1, events.emit returns false and every raisin.http method returns { error, status: 0, ok: false }. Check for error when it matters.

Querying nodes without writing SQL

raisin.nodes.query(workspace, filter) takes a small filter object and returns the matching nodes. Everything in it is bound as a parameter, so a value carrying a quote is a value and not syntax.

const articles = raisin.nodes.query('content', {
nodeType: 'blog:Article',
descendantOf: '/blog',
properties: { status: 'published' },
orderBy: 'created_at',
order: 'desc',
limit: 20,
});

The recognised keys are path, id, nodeType, childOf, descendantOf, properties, orderBy, order, limit and offset. Snake_case spellings work too. Property values are compared as text, because the underlying operator yields text, so write { seq: 0 } and it is matched against '0'.

orderBy accepts path, name, node_type, created_at, updated_at, revision, __order and __tree_order, and rejects anything else. Reach for raisin.sql.query() when the filter object cannot express what you need.

In Starlark the same methods use snake_case names and errors stop the handler:

def handler(input):
print("greeting " + input["name"])
rows = raisin.sql.query("SELECT path FROM 'content'", [])
return {"greeting": "Hello, " + input["name"], "rows": len(rows)}

Invoke a function

Over HTTP, send the input under input. Add "sync": true to run inline and get the result back (the function's execution_mode must be sync or both):

curl -X POST http://localhost:8090/api/functions/myapp/greet/invoke \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"input":{"name":"Ada"},"sync":true}'
{
"execution_id": "BCCB5m3jkMBLtUBsJlA3N",
"sync": true,
"result": { "greeting": "Hello, Ada" },
"duration_ms": 9,
"logs": ["[info] greeting Ada"],
"status": "completed",
"completed": true,
"timed_out": false,
"waited": true
}

Without sync the call queues a job and returns execution_id and job_id immediately. See the Functions API for the full request and response shapes.

From the JavaScript client:

const db = client.database('myapp');
const { result } = await db.functions().invokeSync('greet', { name: 'Ada' });

From SQL:

SELECT INVOKE_SYNC('greet', '{"name":"Ada"}'::jsonb);

Testing a function

raisindb function doctor content/functions/lib/docs/greet
raisindb function run content/functions/lib/docs/greet --input '{"name":"Ada"}' --repo myapp
raisindb function test content/functions/lib/docs/greet --server --repo myapp

doctor checks that the handler named in entry_file exists in the source. run executes the local file on the server and prints the result and logs, without deploying it. test --server replays the scenarios in a hidden .tests.json beside the node (hidden so sync does not upload it as content):

[{ "input": { "name": "Ada" }, "expect": { "greeting": "Hello, Ada" } }]

An object in expect is matched as a subset, so a case asserts only the fields it names. WebAssembly projects additionally have native tests that run with no server at all; see WebAssembly functions.

Next steps