Skip to content

Write and test app functions

Start with a scaffolded app and a sandbox installation.

This guide explains how serverless functions work in Caraer apps today: they are code modules inside one app runtime, not separately provisioned Cloud Functions you edit in the UI.

If you have not scaffolded an app yet, start with How to create a Caraer app.

A function is a named handler in an app. On platform V2 (caraer.json platformVersion: 2026.2.1, the default) the app runs as one async container. Invoking a function is an HTTP call on that container:

{runtimeBaseUrl}/functions/{name}

Locally, caraer apps local dev mirrors the same contract at POST /functions/{name}.

Handler files belong to one versioned app build. Edit them locally and deploy the app with the CLI. A ready runtime and a successful handler response are separate checks.

Allowed values:

  • nodejs22 (default)
  • python312

On V2 the runtime is set on the app (caraer.json / app.caraer.yaml). Every function in that app uses the same runtime. Validation rejects anything outside the allowlist, and a function may not declare a different runtime than its app.

src/app/functions/<name>.js # Node, or <name>.py for Python
src/app/shared/ # require("../shared") from a function file

The filename is the function name. 2026.2 still uses functions/<name>/index.js. The next caraer apps push rewrites that to the flat 2026.2.1 file unless you pass --no-upgrade.

Terminal window
caraer apps add function catch-created
caraer apps add options-function list-calendars

Shared helpers must use the same relative import locally and in the deployed archive. Copying the same file into every function folder will drift.

Node:

export const handler = async (req, res) => {
const body = req.body || {};
return res.status(200).json({ ok: true });
};

Python:

def handler(request):
body = request.get("body") or {}
return {"statusCode": 200, "body": {"ok": True}}

The platform sends a JSON envelope (also req.body in Node):

{
"event": "Installed",
"appUuid": "...",
"companyUuid": "...",
"installationToken": "...",
"caraerApiBase": "https://api.caraer.com/api",
"functionName": "on-install",
"settingsSchema": [{ "name": "inbox_label", "type": "SINGLE_LINE", "value": "Main" }],
"secrets": {},
"connections": [],
"userSettings": {},
"payload": {}
}

Read installer settings from the flattened settingsSchema (name → value). Call Caraer APIs with body.caraerApiBase and body.installationToken — a short-lived inst_… Bearer (about one hour), issued for NONE, API_KEY, and OAUTH2 installs. It is not the long-lived API key. Use /v2/apps/{appUuid}/installation/state, /secrets, and /jobs for cursors, tokens, and async work.

Typed payloads ship in @caraer/client (Node) and caraer-client (Python) — LifecyclePayload, WebhookPayload, SchedulePayload.

You do not register a raw GCP URL. You declare a trigger that names the function. On push, the CLI resolves serverlessFunction.name to the remote function.

Trigger Files Typical topic / route
Platform webhook src/app/webhooks/*.json record.{object}.created, .updated, .deleted, .all
Lifecycle hook src/app/lifecycle/*.json app.installed, app.updated, app.uninstalled, app.rotated
Schedule src/app/schedules/*.json Spring cron → function
Inbound HTTP src/app/inbound/*.json POST /api/v2/public/apps/{appUuid}/inbound/{name}
App bar appBars on the manifest app.bar.triggered
Job POST .../installation/jobs { functionName, payload, delaySeconds? }
Options loader optionsSource.serverlessFunctionName loadSettingOptions

Delivery modes:

  • SERVERLESS — Caraer invokes the function in the app container. Use this unless you already have an external HTTP receiver.
  • HTTP — Caraer POSTs the same envelope to a URL you host.

Inbound routes default to enqueue: true and return 202 { "jobId": "..." }. The function then runs as a job. Webhook and inbound handlers should return quickly and enqueue more work when a run can exceed the function timeout (default 60s).

The production inbound URL is not shown in the Caraer UI. Assemble POST {caraerApiBase}/v2/public/apps/{appUuid}/inbound/{name}?companyUuid={companyUuid} (or send X-Caraer-Company-Uuid). Pass caller data in the JSON body — it arrives as body on the function payload (action: app.inbound). Extra query params are not forwarded. See How to create a Caraer app.

Lifecycle hooks can set waitUntilComplete: true so install or settings-save waits for the function (for example to write mappings back). The default is fire-and-forget.

Terminal window
caraer apps add webhook --topic record.candidate.created --function catch-created
caraer apps add schedule renew-watch --function renew-watch --cron "0 0 */6 * * *"
caraer apps add inbound gmail-push --function gmail-push --auth SHARED_SECRET
caraer apps add lifecycle-hook install
Terminal window
caraer apps validate
caraer apps push --dry-run
caraer apps push

--deploy uploads a developer-project build archive, starts a deploy, and polls App.runtimeStatus until READY or FAILED. caraer apps status shows runtime, runtimeStatus, and runtimeBaseUrl.

Saving the app document in the API or UI does not provision a new Cloud Function per handler on V2. That save-to-provision behavior was the V1 model and is why older notes treated function edits as infrastructure changes.

Terminal window
caraer apps local dev
caraer apps local test catch-created --record <record-uuid> --sample-only
caraer apps local logs --all --follow

local test --sample-only fetches a sample for a real record; it does not invoke the handler. Omitting --sample-only invokes the remote function.

local dev emulates installation state, secrets, and jobs. Use it to exercise handlers without waiting for a container rebuild. Real record webhooks, inbound URLs, and external OAuth still need a deployed install.

  • Keep handlers idempotent. Webhooks and jobs can retry.
  • Guard against duplicate events (store a cursor or event id in installation state).
  • Enqueue a job for anything that cannot finish inside the timeout.
  • Version behavior with caraer apps push and release notes — not by editing code in the UI.
  • Watch runtimeStatus and caraer apps local logs --all after a deploy.
  • Put secrets in root .env (packed as runtime environment) or installation secrets. Never commit them.