Serverless functions
Write TypeScript or JavaScript that SalesOS runs for you in an isolated sandbox — from workflows, custom actions, events, schedules, HTTP and the save path.
A serverless function is code you write and SalesOS runs — no server to host. Use one where a custom action would need an endpoint of your own: score a lead, call a partner API when a deal closes, clean data nightly, or answer a webhook-style request from another system.
Functions are part of the Enterprise plan. Manage them under Settings →
Developer → Functions, or with /v1/functions (org owners and admins).
A first function
Export a default async (input, context) handler (or set module.exports).
What it returns must be JSON; it becomes the invocation's output.
What the code can use
There are no imports and no Node.js APIs — no require, process, fs,
timers or environment. The only capabilities are these globals:
| Global | What it does |
|---|---|
input / context | The handler's arguments. context has organizationId, functionId, functionName, invocationId, trigger, version, attempt, and record or event when relevant. |
env | The function's environment variables (plain configuration; visible to admins). |
secrets.get(name) | One of the function's secrets. Values are encrypted at rest and never shown again after you set them. |
log.info / warn / error / debug, console.* | Captured into the invocation log (stdout / stderr, 64 KB each). |
fetch(url, init) | HTTPS only, to hosts on the function's network allow-list (api.example.com or *.example.com). Private, internal and reserved addresses are refused even if a listed name resolves to one. Text bodies; 256 KB requests, 1 MB responses; 20 calls per invocation; 10 s each; up to 2 redirects, each re-checked. |
salesos.* | The CRM, as the function's run-as member (below). |
fetch('callout:<name>/path', init), salesos.callout(name, path, init) | A call through one of your named credentials: SalesOS adds its authentication, so the function never sees the secret, and the credential's base URL is the allow-list (no allowedHosts entry needed). Same limits and response shape as fetch. |
salesos.events.publish(type, payload) | Publishes one of your custom events (custom.<apiName>); resolves to { eventId, type, version }. |
salesos.*
leads, contacts, accounts, opportunities, tasks and
records('<object>') (including quote for reads and custom:<apiName>)
offer get, list (returns { nodes, pageInfo: { page, limit, total, totalPages, hasMore } }),
create, update and delete; graphql(query, variables) runs any GraphQL query or mutation. Every
call goes through the same services as the REST API:
- It acts as the run-as member (the function's creator), in your organization only, with that member's visibility, sharing, Profile and field-level security. An admin's admin visibility is dropped unless you turn on Keep admin visibility.
- It holds only the scopes you declare (
READ_LEADS,WRITE_LEADS, …); nothing is granted by default. - Writes run validation rules, before-save hooks, workflows, webhooks and the
audit trail. Errors carry the REST codes (
VALIDATION_RULE_FAILED, …) inerror.code.
Triggers
| Trigger | How |
|---|---|
| Workflow step | Add a Function step in Automations. The function gets { record, object }; map outputs back to fields (score=leadScore). The write-back is made as the workflow's creator through the entity services, and can't re-trigger the same workflow. |
| Custom action | Point a custom action at a function instead of a URL. Its input and output schemas, workflow step, assistant tool and Run action button work unchanged. |
| Events | Subscribe to catalog event types or your custom events; input is the event envelope ({ id, type, version, occurredAt, organizationId, data }). Runs are asynchronous; a failure is retried up to 3 times with backoff (about 30 s, 2 min, 8 min). A function never receives events caused by its own writes. |
| Schedule | A five-field cron expression in UTC, at most every 5 minutes. input is { scheduledAt, schedule }. |
| HTTP | Turn on the endpoint, then POST /v1/functions/{id or apiName}/invoke with { "input": {...} }. |
| Before save | A before-save hook can be decided by a function: same payload and { allow, patch, message } contract, the hook's timeout (≤ 5 s), synchronous. |
| After save | An after-save trigger runs the function synchronously after a save commits, with { operation, object, record, previous, changedFields }. It cannot undo the save; a failure is a response warning. |
HTTP invocation
- API keys and OAuth tokens need the
INVOKE_FUNCTIONSscope, and act as their owner, who — like a session caller — must be an org owner or admin. - The function must have its HTTP endpoint turned on (
403 FUNCTION_HTTP_DISABLED). - A function's own errors are a
200withok: falseanderror.code(FUNCTION_ERROR,FUNCTION_INACTIVE,NO_ACTIVE_VERSION,TIMEOUT,CPU_LIMIT,MEMORY_LIMIT,HOST_CALL_LIMIT,RESULT_TOO_LARGE, …).429means the sandbox is busy (FUNCTIONS_BUSY,FUNCTION_CONCURRENCY_LIMIT) — retry later. - Outbound
fetchcalls carryX-SalesOS-Function-Depth. A function calling functions (through this endpoint) is stopped at depth 3 with508 FUNCTION_RECURSION_LIMIT.
Versions and deploys
Every save creates an immutable version (compiled immediately; syntax errors and
imports are rejected with FUNCTION_COMPILE_FAILED). Deploying makes a version
the one that runs; roll back re-deploys the version before it.
| Endpoint | |
|---|---|
POST /v1/functions/:id/versions { source, language, note?, deploy? } | Save a version |
GET /v1/functions/:id/versions | Versions, newest first (paged) |
POST /v1/functions/:id/deploy { versionId } | Deploy a version |
POST /v1/functions/:id/rollback | Deploy the previous version |
PUT /v1/functions/:id/secrets/:NAME { value } / DELETE | Set or remove a secret |
POST /v1/functions/:id/test { input, source?, versionId? } | Run once (unsaved code allowed) and get logs back |
GET /v1/functions/:id/invocations | The invocation log (paged; status, trigger filters) |
From the CLI
Keep functions in a metadata project and work locally:
function.json holds the settings (triggers, limits, allow-list, scopes) and
secret names; values are set per org with
salesos functions secrets <apiName> set NAME. A new function runs as the member
who deploys it.
Limits
| Default | Maximum | |
|---|---|---|
Wall-clock time (includes fetch and salesos calls) | 10 s | 30 s |
| CPU time | same as the time limit | 30 s |
| Memory | 128 MB (16 MB minimum) | 128 MB |
| Return value | 256 KB | |
| Logs | 64 KB per stream | |
Host calls (fetch, secrets, salesos) | 100 per invocation | |
| Source | 256 KB | |
| Running at once | 4 per organization | |
| Functions | 25 per organization | |
| Secrets | 20 per function, 8 KB each | |
| Environment variables | 50 per function, 4,096 characters each | |
| Network allow-list | 20 hosts (no IP addresses) | |
| Event types | 20 per function | |
apiName | ^[a-z][a-z0-9_]{1,47}$ |
Invocations are kept for 14 days.
Security model
What SalesOS guarantees about where your code runs:
- Isolated from SalesOS and from other tenants. Each invocation starts in a fresh, empty sandbox outside the API server. It has no file system, no environment, no database connection and no credentials — only the globals listed above. Nothing persists between invocations.
- No network except what you allow. Code has no direct network access.
fetchis performed by SalesOS on the function's behalf, only to hosts on its allow-list (or under a named credential's base URL), and never to private, internal or reserved addresses. - Data crosses as JSON only. Your code never holds a reference to a SalesOS
object; every
salesos.*call is checked as the run-as member with the function's declared scopes. - Limits are enforced, not advisory. A run that exceeds its time, CPU,
memory, output or host-call limit is stopped and reported with the matching
status (
TIMEOUT,MEMORY_LIMIT, …).
Before-save hooks
A synchronous call to your endpoint before a record is created, updated or deleted — allow it, reject it with a message, or patch it.
Flows
Guided screen flows and autolaunched processes built without code — run from records, lists, pages, a link, workflows or events, and through the API.