SalesOS

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 default async function handler(
  input: { email?: string },
  context: { trigger: string; invocationId: string },
) {
  const key = await secrets.get('CLEARBIT_KEY');
  const res = await fetch(`https://person.clearbit.com/v2/people/find?email=${input.email}`, {
    headers: { Authorization: `Bearer ${key}` },
  });
  if (!res.ok) throw new Error(`Enrichment failed: ${res.status}`);
  const person = await res.json();
  log.info('enriched', input.email);
  return { title: person.employment?.title ?? null };
}

Export a default async (input, context) handler (or set module.exports). What it returns must be JSON; it becomes the invocation's output.

# Create it (version 1 is compiled and deployed)
curl -X POST https://api.salesos.org/v1/functions \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "apiName": "enrich_person", "name": "Enrich person", "language": "typescript",
        "source": "export default async (input) => ({ echo: input })",
        "allowedHosts": ["person.clearbit.com"], "scopes": ["READ_LEADS", "WRITE_LEADS"] }'

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:

GlobalWhat it does
input / contextThe handler's arguments. context has organizationId, functionId, functionName, invocationId, trigger, version, attempt, and record or event when relevant.
envThe 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.*

const lead = await salesos.leads.get(input.leadId);
await salesos.leads.update(lead.id, { rating: 'HOT', customFields: { segment: 'enterprise' } });
const open = await salesos.opportunities.list({ isClosed: false, limit: 50 });
const project = await salesos.records('custom:project').create({ title: 'Kickoff' });
const res = await salesos.graphql('{ accounts(limit: 5) { nodes { id name } } }');

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, …) in error.code.

Triggers

TriggerHow
Workflow stepAdd 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 actionPoint 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.
EventsSubscribe 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.
ScheduleA five-field cron expression in UTC, at most every 5 minutes. input is { scheduledAt, schedule }.
HTTPTurn on the endpoint, then POST /v1/functions/{id or apiName}/invoke with { "input": {...} }.
Before saveA before-save hook can be decided by a function: same payload and { allow, patch, message } contract, the hook's timeout (≤ 5 s), synchronous.
After saveAn 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

curl -X POST https://api.salesos.org/v1/functions/enrich_person/invoke \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c…" \
  -d '{ "input": { "email": "[email protected]" } }'
{ "invocationId": "fni_…", "status": "SUCCEEDED", "ok": true,
  "output": { "title": "CTO" }, "error": null, "durationMs": 412, "version": 3 }
  • API keys and OAuth tokens need the INVOKE_FUNCTIONS scope, 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 200 with ok: false and error.code (FUNCTION_ERROR, FUNCTION_INACTIVE, NO_ACTIVE_VERSION, TIMEOUT, CPU_LIMIT, MEMORY_LIMIT, HOST_CALL_LIMIT, RESULT_TOO_LARGE, …). 429 means the sandbox is busy (FUNCTIONS_BUSY, FUNCTION_CONCURRENCY_LIMIT) — retry later.
  • Outbound fetch calls carry X-SalesOS-Function-Depth. A function calling functions (through this endpoint) is stopped at depth 3 with 508 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/versionsVersions, newest first (paged)
POST /v1/functions/:id/deploy { versionId }Deploy a version
POST /v1/functions/:id/rollbackDeploy the previous version
PUT /v1/functions/:id/secrets/:NAME { value } / DELETESet 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/invocationsThe invocation log (paged; status, trigger filters)

From the CLI

Keep functions in a metadata project and work locally:

salesos functions init score_lead                 # src/functions/score_lead/{function.json,index.ts}
salesos functions deploy score_lead --check-only  # compile + validate, nothing written
salesos functions deploy score_lead --activate
salesos functions invoke score_lead --input @sample.json
salesos functions logs score_lead --follow

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

DefaultMaximum
Wall-clock time (includes fetch and salesos calls)10 s30 s
CPU timesame as the time limit30 s
Memory128 MB (16 MB minimum)128 MB
Return value256 KB
Logs64 KB per stream
Host calls (fetch, secrets, salesos)100 per invocation
Source256 KB
Running at once4 per organization
Functions25 per organization
Secrets20 per function, 8 KB each
Environment variables50 per function, 4,096 characters each
Network allow-list20 hosts (no IP addresses)
Event types20 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. fetch is 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, …).

On this page