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.
What a function is
Section titled “What a function is”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.
Runtimes
Section titled “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
Section titled “Project layout”src/app/functions/<name>.js # Node, or <name>.py for Pythonsrc/app/shared/ # require("../shared") from a function fileThe 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.
caraer apps add function catch-createdcaraer 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.
Handler contract
Section titled “Handler contract”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.
How functions get invoked
Section titled “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.
| 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-createdcaraer apps add schedule renew-watch --function renew-watch --cron "0 0 */6 * * *"caraer apps add inbound gmail-push --function gmail-push --auth SHARED_SECRETcaraer apps add lifecycle-hook installDeploy
Section titled “Deploy”caraer apps validatecaraer apps push --dry-runcaraer 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.
Local loop and sample payloads
Section titled “Local loop and sample payloads”caraer apps local devcaraer apps local test catch-created --record <record-uuid> --sample-onlycaraer apps local logs --all --followlocal 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.
Operational checklist
Section titled “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 pushand 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.
Related reading
Section titled “Related reading”- How to create a Caraer app — includes external OAuth and installation secrets
- App bars in Caraer apps
- Caraer CLI