If you aren’t interested in a code-forward approach, we recommend starting with AutoBuild, which uses AI to analyze your template or documentation and then automatically creates and deploys a Blueprint (for schema definition) and a Listener (for validations and transformations) to your Flatfile App.

Once you’ve started with AutoBuild, you can always download your Listener code and continue building with code from there!

About Listeners

Listeners handle different types of events in Flatfile:

  • Space Configuration: Setting up workbooks, sheets, and themes
  • Data Validation: Custom field and record-level validation
  • Job Processing: Handling long-running operations
  • File Operations: Processing uploaded files
  • User Interactions: Responding to custom actions

For a thorough explanation of listeners and how they fit into the Flatfile ecosystem, see Events and Listeners.

Install Dependencies

Install the required packages for building Flatfile listeners:

npm install @flatfile/listener @flatfile/api

Create Your Listener File

Create a new file called listener.js (or listener.ts for TypeScript):

import { FlatfileListener } from "@flatfile/listener";
import api from "@flatfile/api";

export default function (listener) {
  // Configure the space when it's created
  listener.on("space:configure", async (event) => {
    const spaceId = event.context.spaceId;

    await api.workbooks.create({
      spaceId,
      name: "My Workbook",
      sheets: [
        {
          name: "contacts",
          slug: "contacts",
          fields: [
            { key: "name", type: "string", label: "Full Name" },
            { key: "email", type: "string", label: "Email" },
          ],
        },
      ],
      actions: [
        {
          label: "Submit",
          description: "Send data to destination system",
          operation: "submitActionForeground",
          mode: "foreground",
        },
      ],
    });
  });

  // Handle when someone clicks Submit
  listener.on(
    "job:ready",
    { job: "workbook:submitActionForeground" },
    async (event) => {
      const { jobId } = event.context;

      // Get the data
      const job = await api.jobs.get(jobId);
      const records = await api.records.get(job.data.workbookId);

      // Process it (log to console for now)
      console.log("Processing records:", records.data.length);

      // Mark job as complete
      await api.jobs.complete(jobId, {
        outcome: { message: "Data processed successfully!" },
      });
    }
  );
}

Testing and Deployment

For complete testing and deployment documentation, see the CLI Reference.

Authentication Setup

For complete authentication setup examples, see the Authentication Examples guide.

# .env
FLATFILE_API_KEY=sk_your_api_key_here
FLATFILE_ENVIRONMENT_ID=us_env_your_environment_id

Local Development

To test your listener locally, you can use the flatfile develop command. This will start a local server that will listen for events and respond to them, and will also watch for changes to your listener code and automatically reload the server.

# Run locally with file watching
npx flatfile develop

# With custom entry point
npx flatfile develop ./src/my-listener.ts

Deploy to Flatfile Cloud

Deploying your listener will create a new Agent in your Flatfile environment. Under the hood, this will compile all of your listener code and its dependencies into a single file, which it sends to the Flatfile API’s POST /v1/agents endpoint.

# Basic deployment
npx flatfile deploy

# With custom slug
npx flatfile deploy -s my-listener

That’s it! Your listener will:

  • Create a workbook when a space is opened
  • Process data when users click Submit
  • Handle the complete data import workflow

What Just Happened?

Your minimal listener handles two key events:

  1. space:configure - Sets up the data structure (workbook + sheets)
  2. job:ready - Processes data when users submit

For a more detailed explanation and examples of how to add a custom action, see the Actions guide.

Sheet Validation Example

This example uses a commit:created event listener to detect duplicate (+) email addresses. Records with duplicate emails receive a Warning, while records with emails that appear 3+ times receive an Error.

Because this example checks for duplicates across all records, it needs to retrieve every record from the sheet. The vast majority of validations, however, only need to work with individual records after they’ve been changed – for example, validating the format of an email addresses or ensuring that a date is in the past.

For those scenarios, we recommend using the Record Hook plugin, which provides a faster, cleaner, and more object-oriented approach to validation.

If you use both Record Hooks and regular listener validators (like this one) on the same sheet, you may encounter race conditions. Record Hooks will clear all existing messages before applying new ones, which can interfere with any messages set elsewhere. We have ways to work around this, but it’s a good idea to avoid using both at the same time.

listener.on("commit:created", async (event) => {
  const response = await api.records.get(event.context.sheetId, { includeMessages: true });
  const records = response.data.records;
  
  // Create a map to count email occurrences
  const emailCounts = new Map();
  
  // Count all email addresses
  for (const record of records) {
    const email = record.values.email?.value?.toLowerCase();
    if (email) {
      emailCounts.set(email, (emailCounts.get(email) || 0) + 1);
    }
  }
  
  // Prepare updates for records with duplicate warnings/errors
  const updates = [];
  
  for (const record of records) {
    const email = record.values.email?.value?.toLowerCase();
    if (email) {
      const count = emailCounts.get(email);
      
      if (count === 2) {
        record.values.email.messages.push({ type: "warn", message: "Duplicate email address found" });
        updates.push(record);
      } else if (count >= 3) {
        record.values.email.messages.push({ type: "error", message: "Email address appears multiple times" });
        updates.push(record);
      }
    }
  }
  
  // Update all records at once
  if (updates.length > 0) {
    await api.records.update(event.context.sheetId, updates);
  }
});

Next Steps