In the previous guide, we created a Listener that configures Spaces and sets up the data structure. Now we’ll add data validation to ensure data quality and provide helpful feedback to users as they work with their data.
Following along? Download the starting code from our Getting Started repository and refactor it as we go, or jump directly to the final version with validation.

What Is Data Validation?

Data validation in Flatfile allows you to:
  • Check data formats and business rules
  • Provide warnings and errors to guide users
  • Ensure data quality before processing
  • Give real-time feedback during data entry
Validation can happen at different levels:
  • Field-level: Validate individual Field values (email format, date ranges, etc.)
  • Record-level: Validate relationships between Fields in a single Record
  • Sheet-level: Validate across all Records (duplicates, unique constraints, etc.)

Email Validation Example

This example shows how to perform email format validation directly when Records are committed. When users commit their changes, we validate that email addresses have a proper format and provide helpful feedback for any invalid emails.
This approach validates Records as they’re committed, providing immediate feedback to users. For more complex validations or when you need an object-oriented approach, we recommend using the Record Hook plugin.
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.

What Changes We’re Making

To add validation to our basic Listener, we’ll add a listener that triggers when users commit their changes and performs validation directly:
listener.on("commit:created", async (event) => {
  const { sheetId } = event.context;
  
  // Get committed records and validate email format
  const response = await api.records.get(sheetId);
  const records = response.data.records;
  
  // Email validation logic here...
});

Complete Example with Validation

Here’s how to add email validation to your existing Listener:
import api from "@flatfile/api";

export default function (listener) {
  // Configure the space when it's created
  listener.on("job:ready", { job: "space:configure" }, async (event) => {
    const { jobId, spaceId } = event.context;
    try {
      // Acknowledge the job
      await api.jobs.ack(jobId, {
        info: "Setting up your workspace...",
        progress: 10,
      });
      // Create the workbook with sheets
      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" },
            ],
          },
        ],
      });
      // Update progress
      await api.jobs.update(jobId, {
        info: "Workbook created successfully",
        progress: 75,
      });
      // Complete the job
      await api.jobs.complete(jobId, {
        outcome: {
          message: "Workspace configured successfully!",
          acknowledge: true,
        },
      });
    } catch (error) {
      console.error("Error configuring space:", error);
      // Fail the job if something goes wrong
      await api.jobs.fail(jobId, {
        outcome: {
          message: `Failed to configure workspace: ${error.message}`,
          acknowledge: true,
        },
      });
    }
  });

  // Listen for commits and validate email format
  listener.on("commit:created", async (event) => {
    const { sheetId } = event.context;
    try {
      // Get records from the sheet
      const response = await api.records.get(sheetId);
      const records = response.data.records;
      // Simple email validation regex
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      // Prepare updates for records with invalid emails
      const updates = [];
      for (const record of records) {
        const emailValue = record.values.email?.value;
        if (emailValue) {
          const email = emailValue.toLowerCase();
          if (!emailRegex.test(email)) {
            updates.push({
              id: record.id,
              values: {
                email: {
                  value: email,
                  messages: [
                    {
                      type: "error",
                      message:
                        "Please enter a valid email address (e.g., user@example.com)",
                    },
                  ],
                },
              },
            });
          }
        }
      }
      // Update records with validation messages
      if (updates.length > 0) {
        await api.records.update(sheetId, updates);
      }
    } catch (error) {
      console.error("Error during validation:", error);
    }
  });
}
Complete Example: The full working code for this tutorial step is available in our Getting Started repository: JavaScript | TypeScript

Testing Your Validation

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

Step-by-Step Testing

After running your listener locally:

Testing Steps

  1. Create a new space in your Flatfile environment
  2. Enter an invalid email address in the Email Field
  3. See error messages appear on invalid email Fields
  4. Fix the emails and see the error messages disappear

What Just Happened?

Your Listener now handles two key Events:
  1. space:configure - Sets up the data structure
  2. commit:created - Validates email format when users commit changes
Here’s how the email validation works step by step:

1. Listen for Commits

This listener triggers whenever users save their changes to any sheet in the workbook.
listener.on("commit:created", async (event) => {
  const { sheetId } = event.context;

2. Get the Records

We retrieve all records from the sheet to validate them.
const response = await api.records.get(sheetId);
const records = response.data.records;

3. Validate Email Format

We use a simple regex pattern to check if each email follows the basic user@domain.com format.
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

for (const record of records) {
  const emailValue = record.values.email?.value;
  if (emailValue && !emailRegex.test(emailValue.toLowerCase())) {
    // Add validation error
  }
}

4. Add Error Messages

For invalid emails, we create an update that adds an error message to that specific field.
updates.push({
  id: record.id,
  values: {
    email: {
      value: email,
      messages: [{
        type: "error",
        message: "Please enter a valid email address (e.g., user@example.com)",
      }],
    },
  },
});
You can apply different types of validation messages:
  • info: Informational messages (blue)
  • warn: Warnings that don’t block processing (yellow)
  • error: Errors that should be fixed (red)

5. Update Records

Finally, we send all validation messages back to the sheet so users can see the errors.
if (updates.length > 0) {
  await api.records.update(sheetId, updates);
}

Next Steps

Ready to make your Listener interactive? Continue to Adding Actions to learn how to handle user submissions and create custom workflows. For more detailed information: