In the previous guides, we created a Listener with Space configuration and data validation. Now we’ll extend that Listener to handle user Actions, allowing users to submit and process 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 actions.

What Are Actions?

Actions are interactive buttons that appear in the Flatfile interface, allowing users to trigger custom operations on their data. Common Actions include:
  • Submit: Process your data and POST it to your system via API
  • Validate: Run custom validation rules
  • Transform: Apply data transformations
  • Export: Generate reports or exports

Actions appear as buttons in the Flatfile interface

What Changes We’re Making

To add Actions to our Listener with validation, we need to make two specific changes:

1. Add Actions Array to Blueprint Definition

In the space:configure Listener, we’ll add an actions array to our Workbook creation. This enhances our Blueprint to include interactive elements:
actions: [
  {
    label: "Submit",
    description: "Send data to destination system",
    operation: "submitActionForeground",
    mode: "foreground",
  },
]

2. Add Action Handler Listener

We’ll add a new Listener to handle when users click the Submit button:
listener.on(
  "job:ready",
  { job: "workbook:submitActionForeground" },
  async (event) => {
    // Handle the action...
  }
);

Complete Example with Actions

This example builds on the Listener we created in the previous tutorials. It includes the complete functionality: Space configuration, email validation, and Actions.
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 and Actions
      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",
            primary: true,
          },
        ],
      });
      // 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,
        },
      });
    }
  });
  // Handle when someone clicks Submit
  listener.on(
    "job:ready",
    { job: "workbook:submitActionForeground" },
    async (event) => {
      const { jobId, workbookId } = event.context;
      try {
        // Acknowledge the job
        await api.jobs.ack(jobId, {
          info: "Starting data processing...",
          progress: 10,
        });
        // Get the data
        const job = await api.jobs.get(jobId);
        // Update progress
        await api.jobs.update(jobId, {
          info: "Retrieving records...",
          progress: 30,
        });
        // Get the sheets
        const { data: sheets } = await api.sheets.list({ workbookId });
        // Get and count the records
        const records = {};
        let recordsCount = 0;
        for (const sheet of sheets) {
          const {
            data: { records: sheetRecords },
          } = await api.records.get(sheet.id);
          records[sheet.name] = sheetRecords;
          recordsCount += sheetRecords.length;
        }
        // Update progress
        await api.jobs.update(jobId, {
          info: `Processing ${sheets.length} sheets with ${recordsCount} records...`,
          progress: 60,
        });
        // Process the data (log to console for now)
        console.log("Processing records:", JSON.stringify(records, null, 2));
        // Complete the job
        await api.jobs.complete(jobId, {
          outcome: {
            message: `Successfully processed ${sheets.length} sheets with ${recordsCount} records!`,
            acknowledge: true,
          },
        });
      } catch (error) {
        console.error("Error processing data:", error);
        // Fail the job if something goes wrong
        await api.jobs.fail(jobId, {
          outcome: {
            message: `Data processing failed: ${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

Understanding Action Modes

Actions can run in different modes:
  • foreground: Runs immediately with real-time progress updates (good for quick operations)
  • background: Runs as a background job (good for longer operations)
The Action operation name (submitActionForeground) determines which Listener will handle the Action.

Testing Your Action

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. Upload (or manually enter) some data to the contacts Sheet with both valid and invalid email addresses
  3. See validation errors appear on invalid email Fields
  4. Click the “Submit” button
  5. Watch the logging in the terminal as your data is processed and the job is completed

What Just Happened?

Your Listener now handles three key Events and provides a complete data import workflow. Here’s how the new action handling works:

1. Blueprint Definition with Actions

We enhanced the Blueprint definition to include action buttons that users can interact with. Adding actions to your workbook configuration is part of defining your Blueprint.
actions: [
  {
    label: "Submit",
    description: "Send data to destination system",
    operation: "submitActionForeground",
    mode: "foreground",
    primary: true,
  },
]

2. Listen for Action Events

When users click the Submit button, Flatfile triggers a job that your listener can handle.
listener.on(
  "job:ready",
  { job: "workbook:submitActionForeground" },
  async (event) => {
    const { jobId, workbookId } = event.context;

3. Retrieve and Process Data

Get all the data from the workbook and process it according to your business logic.
// Get the sheets
const { data: sheets } = await api.sheets.list({ workbookId });

// Get and count the records
const records = {};
let recordsCount = 0;
for (const sheet of sheets) {
  const { data: { records: sheetRecords } } = await api.records.get(sheet.id);
  records[sheet.name] = sheetRecords;
  recordsCount += sheetRecords.length;
}

4. Provide User Feedback

Keep users informed about the processing with progress updates and final results.
// Update progress during processing
await api.jobs.update(jobId, {
  info: `Processing ${sheets.length} sheets with ${recordsCount} records...`,
  progress: 60,
});

// Complete with success message
await api.jobs.complete(jobId, {
  outcome: {
    message: `Successfully processed ${sheets.length} sheets with ${recordsCount} records!`,
    acknowledge: true,
  },
});
Your complete Listener now handles:
  • space:configure - Defines the Blueprint with interactive actions
  • commit:created - Validates email format when users commit changes
  • workbook:submitActionForeground - Processes data when users click Submit
The Action follows the same Job lifecycle pattern: acknowledge → update progress → complete (or fail on error). This provides users with real-time feedback during data processing, while validation ensures data quality throughout the import process.

Next Steps

Congratulations! You now have a complete Listener that handles Space configuration, data validation, and user Actions. For more detailed information: