Server Functions (Node.js)
For creating server functions, the detail view UI, security configuration, and logs, see Server Functions in the User Guide.
Server Functions run as Node.js modules outside the database. Use them for work that cannot run inside PostgreSQL: calling external HTTP APIs, sending email, working with AI models, handling files, processing webhooks, or anything that may take time or cross a network boundary. They share the same api data access surface as Database Workflows but run asynchronously and hold no database connection for their full duration.
Module structure
Section titled “Module structure”Every Server Function follows a four-section layout: METADATA, CONSTANTS, PRIVATE_FUNCTIONS, and HANDLERS. When a new function is created in the platform, the editor is pre-populated with a working template following this structure.
Command dispatch
Section titled “Command dispatch”- When the caller includes a
cmdfield, the platform invokes the handler with that name. - When
cmdis omitted, therunhandler is called. - Each handler receives
input- the request payload withcmdremoved. - Handler names use snake_case.
- Prefer
asynchandlers for any I/O.
The __meta object
Section titled “The __meta object”The __meta block declares this function’s identity and dependencies:
| Field | Description |
|---|---|
thing_id | Stable UUID for this function - required |
can_delete | Whether records created by this function can be deleted |
function_targets | Thing IDs of other functions called via api.execute_function |
view_targets | Thing IDs of views read via api.get_data |
entity_targets | Thing IDs of entities read or written |
component_targets | Thing IDs of components read or written |
The AI assistant keeps __meta up to date automatically as you develop. You can also ask it explicitly: “update the meta object to reflect all targets in this code.”
JSDoc and published methods
Section titled “JSDoc and published methods”Adding JSDoc to handlers is how the platform discovers the function’s public interface at publish time:
/** * Send a confirmation email for an order. * @param {{ thing_id: string, recipient_email: string }} input * @returns {Promise<void>} */handlers.send_confirmation = async input => { // ...};After publishing, the platform extracts handler names, descriptions, and parameter shapes. This powers the Published methods picker in Run Function and the method selector in workflow Execute Server Function steps. The AI can write JSDoc for you - ask it to “add JSDoc to all handlers.”
Using AI to write Node.js code
Section titled “Using AI to write Node.js code”The AI/Code tab opens in chat mode. Describe what the function should do and the AI generates Node.js code following the structure above, including JSDoc on handlers. Click Apply to push a suggestion into the editor.
Before prompting, add the entities, views, and functions the AI should work with via the Context picker. The AI does not auto-discover your data model - without context it cannot reference the correct fields, view parameters, or function signatures. The more relevant context you add, the more accurate the generated code will be.
Use See Code to review or modify code directly. Switch back to Use AI at any time to continue iterating.
The AI is aware of the full api.* surface available in Node - including api.http, api.ai, api.files, api.mail, and api.secrets. It will suggest the appropriate method for the task and handle the async/await pattern correctly.
api overview
Section titled “api overview”api is available automatically - no import needed. All platform operations require await:
const rows = await api.get_data({ target: constants.order_status_view, params: { thing_id: input.thing_id }});Data access
Section titled “Data access”The data access methods are identical to those in Database Workflows, with one difference: all calls require await.
// Load from a viewconst rows = await api.get_data({ target: view_id, params: {} });
// Query an entity/component directlyconst records = await api.query_data({ target: entity_id, filter: `status = ` + await api.quote_literal(api.constants.statuses.live)});
// Save, submit, delete, cloneawait api.save_data({ target: entity_id, item: record });await api.save_and_submit({ target: entity_id, item: record });await api.submit({ target: entity_id, item: { thing_id, version_id } });await api.delete_data({ target: entity_id, item: { thing_id, version_id } });
const cloned = await api.clone_data({ source: { thing_id }, item: template });const draft = await api.clone_version({ source: { thing_id, version_id } });
// Call another functionconst result = await api.execute_function({ target: constants.my_fn, cmd: `handler_name`, input: { thing_id }});See Database Workflows - Data access for the full field reference on each method.
Context
Section titled “Context”const user = await api.current_user();if (user.anonymous) { throw new Error(`Authentication required`);}
if (await api.is_feature_enabled(`my_feature`)) { // ...}api.constants provides the same statuses and entity_types reference values as in PLV8:
filter: `status = ` + await api.quote_literal(api.constants.statuses.live)HTTP (api.http)
Section titled “HTTP (api.http)”Methods
Section titled “Methods”await api.http.get(input)await api.http.post(input)await api.http.put(input)await api.http.patch(input)await api.http.delete(input)await api.http.head(input)await api.http.get_stream(input)/await api.http.post_stream(input)
| Field | Required | Description |
|---|---|---|
url | yes | Request URL |
headers | no | Headers object |
query | no | Query string parameters object |
body | no | Request body |
timeout_ms | no | Timeout in milliseconds |
response_type | no | json (default), text, xml, form, html |
full_response | no | When true, return full response with status and headers |
auth | no | { type: 'basic', username, password } or { type: 'bearer', token } |
on_data | stream only | Required callback for stream methods |
const data = await api.http.post({ url: `https://api.example.com/orders`, headers: { Accept: `application/json` }, body: { order_id: input.order_id }, timeout_ms: 30000});Custom Route handlers
Section titled “Custom Route handlers”Server functions can be configured as handlers for Custom Routes - HTTP endpoints for receiving webhooks, serving API responses, or handling file downloads. When invoked this way, the input includes req and res objects for reading the request and writing the response. These are not present when a function is called from a Database Workflow, a portal action, or the platform UI.
Implement a process_request handler as the entry point. If process_request is not defined, the platform falls back to run. Use exactly one response pattern per request.
Handler shape
Section titled “Handler shape”handlers.process_request = async input => { const { req, body } = input;
if (req.method !== `POST`) { return api.http_error.method_not_allowed(`Method not allowed`); }
const result = await do_work(body); return { ok: true, data: result };};input includes: req (method, headers, path), res, query (parsed query string), body (parsed request body).
Response patterns
Section titled “Response patterns”Return-driven - return JSON or an api.http_error. Do not write to input.res.
return { ok: true, id: created.thing_id };return api.http_error.bad_request(`Missing required field`);Response-driven - write directly to input.res via api.webhook helpers. Return nothing after the response is written.
await api.webhook.stream_file({ res: input.res, file_path: export_path, content_type: `text/csv`, content_disposition: `attachment; filename=export.csv`});
await api.webhook.redirect({ res: input.res, location: redirect_url, status: 302 });
await api.webhook.send({ res: input.res, content_type: `text/plain`, body: `ok` });HTTP error helpers
Section titled “HTTP error helpers”| Method | Status |
|---|---|
api.http_error.bad_request(msg) | 400 |
api.http_error.unauthorized(msg) | 401 |
api.http_error.forbidden(msg) | 403 |
api.http_error.not_found(msg) | 404 |
api.http_error.method_not_allowed(msg) | 405 |
api.http_error.conflict(msg) | 409 |
api.http_error.unprocessable_entity(msg) | 422 |
api.http_error.too_many_requests(msg) | 429 |
api.http_error.internal(msg) | 500 |
api.http_error.custom(status, code, msg, details?) | any |
AI (api.ai)
Section titled “AI (api.ai)”api.ai.generate(input)
Section titled “api.ai.generate(input)”Text and multimodal generation.
| Field | Description |
|---|---|
messages | Array of message items (preferred) |
system | System prompt |
model, provider | Default provider: openai |
options | Provider-specific options (e.g. temperature) |
stream: true + on_stream | Enable streaming with a chunk callback |
Returns { success, data, meta, error }. Check success before using data:
const result = await api.ai.generate({ messages: [`Summarize this order in one sentence.`, JSON.stringify(order)], options: { temperature: 0.2 }});
if (!result.success) { throw new Error(`AI failed: ${result.error.message}`);}
return result.data;api.ai.embed(input)
Section titled “api.ai.embed(input)”Generate embeddings. input is a string or string array. Same envelope as generate.
Secrets (api.secrets)
Section titled “Secrets (api.secrets)”| Method | Description |
|---|---|
await api.secrets.get({ secret_id, encryption_key? }) | Returns the secret value or null |
await api.secrets.set({ secret_id, secret_value, encryption_key?, read_only? }) | Create or update |
await api.secrets.delete({ secret_id }) | Returns true if deleted, false if not found |
const token = await api.secrets.get({ secret_id: `my_integration_token` });if (!token) throw new Error(`Integration token not configured`);Files (api.files)
Section titled “Files (api.files)”api.files.save(input)
Section titled “api.files.save(input)”Required: name, data, mime_type. Optional: extension, description, is_public, encoding, link: { thing_id, version_id }.
Returns file metadata including thing_id, version_id, uri, and public_url_expires.
api.files.save_from_url(input)
Section titled “api.files.save_from_url(input)”Required: url. Downloads and stores the file. Same return shape as save.
api.files.get({ thing_id?, version_id? })
Section titled “api.files.get({ thing_id?, version_id? })”Returns file metadata plus content (inline bytes) or uri (URL-backed).
const file = await api.files.save({ name: `export.csv`, data: csv_content, mime_type: `text/csv`});
await api.mail.send({ to: `user@example.com`, subject: `Your export`, html: `<p>See attached.</p>`, attachments: [file.thing_id]});Mail (api.mail)
Section titled “Mail (api.mail)”api.mail.send(input)
Section titled “api.mail.send(input)”| Field | Required | Description |
|---|---|---|
to | yes | Recipient address or addresses |
subject | yes | Email subject |
html or text | yes | At least one body format |
cc, bcc | no | Additional recipients |
from, from_name | no | Override sender; defaults from platform email settings |
attachments | no | Array of file thing_id values |
ical_event | no | { file_name, content } - calendar invite |
Returns { message_id, accepted, rejected, response }. Throws on failure.
await api.mail.send({ to: `customer@example.com`, subject: `Order confirmed`, html: `<p>Your order has been confirmed.</p>`, attachments: [invoice_file_thing_id]});Logging
Section titled “Logging”| Method | Level |
|---|---|
await api.log({ log_level?, message?, data? }) | configurable (default info) |
await api.log_debug(message?, data?) | debug |
await api.log_info(message?, data?) | info |
await api.log_warn(message?, data?) | warn |
await api.log_error(message?, data?) | error |
await api.log_info(`Order processed`, { thing_id: input.thing_id });
try { await api.save_data({ target: constants.order_entity, item: record });} catch (err) { await api.log_error(`Save failed`, { message: err.message }); throw err;}Log entries appear in the Logs tab of the function detail view in the platform.
Helpers
Section titled “Helpers”const id = await api.new_uuid();const hex = await api.md5(`some_text`);const deterministic_id = await api.md5_uuid(`prefix_` + counter);const copy = await api.clone(original_record);const safe = await api.quote_literal(input.thing_id);Naming conventions
Section titled “Naming conventions”| Kind | Example |
|---|---|
| Handler | get_status, send_confirmation |
| Entity constant | order_entity |
| Component constant | order_lines_component |
| Function constant | submit_order_fn |
| View constant | order_status_view |
| Record variable | order_record |
| Record array | order_records |
General practices
Section titled “General practices”- Keep handlers imperative - avoid classes, repositories, or service layers.
- Validate input early; throw clear errors.
- Always include a
runhandler as the default entry point. - All platform data access goes through
api- no raw SQL or direct DB connections. - Use
await api.quote_literalfor dynamic values inquery_datafilters. - Check
api.ai.*results viasuccess; all otherapimethods throw on failure. - Add JSDoc to handlers before publishing so the platform can discover their interface.