This post 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, 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}.
The graph still stores function metadata (name, runtime, description). For a build-deployed V2 app the build archive is the source of truth for code. The platform does not persist handler source on the function node. Editing function code in the Caraer UI is rejected — change files locally and run caraer apps push --deploy.
Legacy V1 apps (2026.1) still provision one Cloud Function per handler and store code on the node. Do not start new apps that way.
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>/
index.js # Node, or main.py for Python
function.caraer.json # optional overrides (entry, description)
src/app/shared/ # require("../../shared") from a function folderA folder with index.js or main.py is a function named after the folder. function.caraer.json is only needed to override conventions.
caraer apps add function catch-created
caraer apps add options-function list-calendarsShared helpers must use the same relative import locally and in the deployed archive. Copying the same file into every function folder will drift.
Node:
exports.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 both 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.
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 installcaraer apps validate
caraer apps push --dry-run
caraer apps push --deploy--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.
caraer apps local dev
caraer apps local test --function catch-created --sample-only
caraer apps local logs --all --followlocal 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 --deployand release notes — not by editing code in the UI. - Watch
runtimeStatusandcaraer apps local logs --allafter a deploy. - Put secrets in root
.env(packed as runtime environment) or installation secrets. Never commit them.
- How to create a Caraer app — includes external OAuth and installation secrets
- App bars in Caraer apps
- Caraer CLI