---
title: "First Function"
description: "Build, deploy, test, and redeploy your first Primitive Function from the CLI scaffold."
canonical: "https://docs.primitive.dev/docs/functions/first-function"
last-updated: "2026-05-17T00:00:00.000Z"
---

# 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](/docs/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](/docs/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; PRIMITIVE_API_BASE_URL: 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
```

```
CODE=$(cat dist/handler.js | jq -Rs .)


curl -X POST https://api.primitive.dev/v1/functions \
  -H "Authorization: Bearer $PRIMITIVE_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"name\":\"my-fn\",\"code\":$CODE}"
```


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
```

```
curl -X POST https://api.primitive.dev/v1/functions/$PRIMITIVE_FUNCTION_ID/test \
  -H "Authorization: Bearer $PRIMITIVE_API_KEY"
```


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
```

```
curl https://api.primitive.dev/v1/functions/$PRIMITIVE_FUNCTION_ID/logs \
  -H "Authorization: Bearer $PRIMITIVE_API_KEY"
```


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
```

```
CODE=$(cat dist/handler.js | jq -Rs .)


curl -X PUT https://api.primitive.dev/v1/functions/$PRIMITIVE_FUNCTION_ID \
  -H "Authorization: Bearer $PRIMITIVE_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"code\":$CODE}"
```


## 8. Add a Secret


```
primitive functions:set-secret \
  --id $PRIMITIVE_FUNCTION_ID \
  --key EXTERNAL_API_KEY \
  --value value_123 \
  --redeploy
```

```
curl -X POST https://api.primitive.dev/v1/functions/$PRIMITIVE_FUNCTION_ID/secrets \
  -H "Authorization: Bearer $PRIMITIVE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"key":"EXTERNAL_API_KEY","value":"value_123"}'
```


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](/docs/functions) for the full reference.

Maintained by the [primitive.dev team](/about) · [Trust & security](/trust)Last updated: May 17, 2026
