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_ID

The 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_ID

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