First Function
This walkthrough uses the scaffold generated by primitive functions:init. It is the safest way to start because the generated project already uses the expected SDK imports and bundler settings.
Primitive Functions run JavaScript, so Function handler snippets are TypeScript/JavaScript by design. Application SDK examples across Node.js, Python, and Go live on SDKs.
1. Scaffold
primitive functions:init my-fn cd my-fn
This is local filesystem scaffolding. There is no REST endpoint for creating files on your machine; the REST/curl equivalent starts when you deploy the built bundle.
Generated files include:
handler.ts;build.mjs;package.json;tsconfig.json;.gitignore;README.md.
2. Install and Build
npm install npm run build
The build emits a single ESM bundle at ./dist/handler.js.
3. Inspect the Handler
The scaffolded handler demonstrates the runtime client, handler-side signature verification, and a safe response pattern. Keep the generated imports unless the Functions reference tells you to change them.
import { createPrimitiveClient, normalizeReceivedEmail, PRIMITIVE_SIGNATURE_HEADER, type EmailReceivedEvent, verifyWebhookSignature, WebhookVerificationError, } from '@primitivedotdev/sdk/api'; export default { async fetch( request: Request, env: { PRIMITIVE_API_KEY: string; PRIMITIVE_WEBHOOK_SECRET: string }, ) { const rawBody = await request.text(); const signatureHeader = request.headers.get(PRIMITIVE_SIGNATURE_HEADER) ?? ''; try { await verifyWebhookSignature({ rawBody, signatureHeader, secret: env.PRIMITIVE_WEBHOOK_SECRET, }); } catch (error) { if (error instanceof WebhookVerificationError) { return new Response('invalid signature', { status: 401 }); } throw error; } const client = createPrimitiveClient({ apiKey: env.PRIMITIVE_API_KEY }); const event = JSON.parse(rawBody) as EmailReceivedEvent; if (event.event !== 'email.received') { return Response.json({ ok: true, skipped: event.event }); } await client.reply(normalizeReceivedEmail(event), { text: 'Got your message.', }); return Response.json({ ok: true }); }, };
Keep the generated build pipeline unless you have a concrete reason to change it.
4. Deploy
primitive functions:deploy \ --name my-fn \ --file ./dist/handler.js
The response includes a Function id. Store it as an environment variable for redeploys:
export PRIMITIVE_FUNCTION_ID=<function-id>5. Send a Test Email
primitive functions:test-function --id $PRIMITIVE_FUNCTION_IDThe test travels through the same inbound path as a real email, so it verifies MX ingestion, parsing, endpoint routing, and Function execution.
6. Read Logs
primitive functions:list-function-logs --id $PRIMITIVE_FUNCTION_IDUse logs to confirm deploys and debug handler errors. Avoid logging secrets or full customer content unless you explicitly need it.
7. Redeploy
Edit handler.ts, rebuild, and redeploy:
npm run build primitive functions:redeploy \ --id $PRIMITIVE_FUNCTION_ID \ --file ./dist/handler.js
8. Add a Secret
primitive functions:set-secret \ --id $PRIMITIVE_FUNCTION_ID \ --key EXTERNAL_API_KEY \ --value value_123 \ --redeploy
Read it from env.EXTERNAL_API_KEY inside the handler after redeploying.
What to Build Next
- route inbound messages by recipient address;
- summarize email content with an LLM;
- file issues in an external tracker;
- reply with a status update;
- discard raw content after processing.
See Functions for the full reference.