Skip to content
Authors
  • Sem Tadema
    Sem TademaCTO

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.

Runtimes

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.

Project layout

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 folder

A 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-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.

Handler contract

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.

How functions get invoked

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.

TriggerFilesTypical topic / route
Platform webhooksrc/app/webhooks/*.jsonrecord.{object}.created, .updated, .deleted, .all
Lifecycle hooksrc/app/lifecycle/*.jsonapp.installed, app.updated, app.uninstalled, app.rotated
Schedulesrc/app/schedules/*.jsonSpring cron → function
Inbound HTTPsrc/app/inbound/*.jsonPOST /api/v2/public/apps/{appUuid}/inbound/{name}
App barappBars on the manifestapp.bar.triggered
JobPOST .../installation/jobs{ functionName, payload, delaySeconds? }
Options loaderoptionsSource.serverlessFunctionNameloadSettingOptions

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 install

Deploy

caraer 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.

Local loop

caraer apps local dev
caraer apps local test --function catch-created --sample-only
caraer apps local logs --all --follow

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.

Operational checklist

  • 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 --deploy 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.