Using actions
An Action is a code-based operation that runs where that Action is mounted. Actions run when a user clicks the corresponding user prompt in Flatfile.
Overview
Workbook & Sheet-mounted Actions are configured within a BlueprintA Data Definition Language... object, while File-mounted actions are appended to the file during the upload process.
The executable code within an Action is compiled into a JobLarge asynchronous work... entity, providing the capability to run asynchronously or immediately.
Sheet-mounted actions can be executed on the entire Sheet, for a filtered view of the Sheet, or selectively for the chosen records.
Workbook-mounted
Once a user has extracted and mapped data into a WorkbookAnalogous to a database..., it may be more efficient to run an operation on the entire dataset rather than making atomic transformations at the record- or field-level.
For example:
- Sending a webhook that notifies your API of the data’s readiness
- Populating a Sheet with data from another source
- Adding two different fields together after a user review’s initial validation checks
- Moving valid data from an editable Sheet to a read-only Sheet
Workbook-mounted actions are represented as buttons in the top right of the Workbook.
Usage
First, configure your action on your Blueprint.
If you configure primary: true
on an Action, it will be represented as the
rightmost button in the Workbook.
If you configure trackChanges: true
, it will disable your actions
until all commits are complete (usually data hooks).
If you configure constraints: [{ type: 'hasAllValid' }]
on an Action, it will
disable a Workbook Action when there are invalid records.
//your workbook should have an action that looks like this
actions: [
{
operation: 'submitActionFg',
mode: 'foreground',
label: 'Submit data elsewhere',
type: 'string',
description: 'Submit this data to a webhook.',
primary: true,
constraints: [{ type: 'hasAllValid' }]
},
{...}
],
settings: {
trackChanges: true,
}
Next, listen for a job:ready
and filter on the job
you’d like to process. Be sure to complete the job when it’s complete.
import api from "@flatfile/api";
import { responseRejectionHandler } from "@flatfile/util-response-rejection";
import axios from "axios";
export default function flatfileEventListener(listener) {
listener.on(
"job:ready",
{ job: "workbook:submitActionFg" },
async ({ context: { jobId, workbookId }, payload }) => {
const { data: workbook } = await api.workbooks.get(workbookId);
const { data: workbookSheets } = await api.sheets.list({ workbookId });
const sheets = [];
for (const [_, element] of workbookSheets.entries()) {
const { data: records } = await api.records.get(element.id);
sheets.push({
...element,
...records,
});
}
try {
await api.jobs.ack(jobId, {
info: "Starting job to submit action to webhook.site",
progress: 10,
});
console.log(JSON.stringify(sheets, null, 2));
const webhookReceiver =
process.env.WEBHOOK_SITE_URL ||
"https://webhook.site/c83648d4-bf0c-4bb1-acb7-9c170dad4388"; //update this
const response = await axios.post(
webhookReceiver,
{
...payload,
method: "axios",
workbook: {
...workbook,
sheets,
},
},
{
headers: {
"Content-Type": "application/json",
},
}
);
if (response.status === 200) {
const rejections = response.data.rejections;
if (rejections) {
const totalRejectedRecords = await responseRejectionHandler(
rejections
);
return await api.jobs.complete(jobId, {
outcome: {
next: {
type: "id",
id: rejections.id,
label: "See rejections...",
},
message: `Data was submission was partially successful. ${totalRejectedRecords} record(s) were rejected.`,
},
});
}
return await api.jobs.complete(jobId, {
outcome: {
message:
"Data was successfully submitted to webhook.site. Go check it out at " +
webhookReceiver +
".",
},
});
} else {
throw new Error("Failed to submit data to webhook.site");
}
} catch (error) {
console.log(`webhook.site[error]: ${JSON.stringify(error, null, 2)}`);
await api.jobs.fail(jobId, {
outcome: {
message:
"This job failed probably because it couldn't find the webhook.site URL.",
},
});
}
}
);
}
// See full code example (https://github.com/FlatFilers/flatfile-docs-kitchen-sink/blob/main/javascript/shared/workbook_submit.js)
Document-mounted
Document-mounted actions are similar to Workbook-mounted actions. They appear in the top right corner of your Document:
Read more about Documents here.
Usage
Define Document-mounted Actions using the actions
parameter when you create a Document.
If you configure primary: true
on an Action, it will be represented as the
rightmost button in the Document.
import api from "@flatfile/api";
export default function flatfileEventListener(listener) {
listener.on("upload:completed", async ({ context: { spaceId, fileId } }) => {
const fileName = (await api.files.get(fileId)).data.name;
const bodyText = `# Welcome
### Say hello to your first customer Space in the new Flatfile!
Let's begin by first getting acquainted with what you're seeing in your Space initially.
---
Your uploaded file, ${fileName}, is located in the Files area.`;
const doc = await api.documents.create(spaceId, {
title: "Getting Started",
body: bodyText,
actions: [
{
label: "Submit",
operation: "contacts:submit",
description: "Would you like to submit the contact data?",
tooltip: "Submit the contact data",
mode: "foreground",
primary: true,
confirm: true,
},
],
});
});
}
See full code example in our flatfile-docs-kitchen-sink Github repo.
In your listener, listen for the job’s event and perform your desired operations.
export default function flatfileEventListener(listener) {
listener.on(
"job:ready",
{ job: "document:contacts:submit" },
async (event) => {
const { context, payload } = event;
const { jobId, workbookId } = context;
try {
await api.jobs.ack(jobId, {
info: "Starting submit job...",
progress: 10,
estimatedCompletionAt: new Date("Tue Aug 23 2023 16:19:42 GMT-0700"),
});
// Do your work here
await api.jobs.complete(jobId, {
outcome: {
message: `Submit job was completed succesfully.`,
},
});
} catch (error) {
console.log(`There was an error: ${JSON.stringify(error)}`);
await api.jobs.fail(jobId, {
outcome: {
message: `This job failed.`,
},
});
}
}
);
}
Sheet-mounted
Sheet-mounted actions are represented as a dropdown in the toolbar of the Sheet.
Usage
First, configure your action on your Blueprint.
If you configure constraints: [{ type: 'hasSelection' }]
on an Action, it
will disable a Sheet Action when no records in the Sheet are selected.
If you configure constraints: [{ type: 'hasAllValid' }]
on an Action, it will
disable a Sheet Action when there are invalid records.
sheets : [
{
name: "Sheet Name",
actions: [
{
operation: 'duplicate',
mode: 'background',
label: 'Duplicate selected names',
type: 'string',
description: 'Duplicate names for selected rows',
constraints: [{ type: 'hasAllValid' }, { type: 'hasSelection' }],
primary: true,
},
{...}
]
}
]
Next, listen for a job:ready
and filter on the domain
(sheet) and the
operation
of where the action was placed. Be sure to complete to job when
it’s complete.
listener.on(
"job:ready",
{ job: "sheet:duplicate" },
async ({ context: { jobId } }) => {
try {
await api.jobs.ack(jobId, {
info: "Getting started.",
progress: 10,
estimatedCompletionAt: new Date("Tue Aug 23 2023 16:19:42 GMT-0700"),
});
// Do your work here
await api.jobs.complete(jobId, {
info: "This job is now complete.",
});
} catch (error) {
console.error("Error:", error.stack);
await api.jobs.fail(jobId, {
info: "This job did not work.",
});
}
}
);
Get the example in the getting-started repo
Retrieving data
Data from the Sheet can be retrieved either by calling the API with records.get
or through data passed in through event.data
. Here are some examples demonstrating how you can extract data from a Sheet-mounted action:
From the entire Sheet
This method allows you to access and process data from the complete Sheet, regardless of the current view or selected records.
//inside listener.on()
const { jobId, sheetId } = event.context;
//retrieve all records from sheet
const { records } = await api.records.get(sheetId);
//print records
console.log(records.data.records);
From a filtered view of the Sheet
By applying filters to the Sheet, you can narrow down the data based on specific criteria. This enables you to retrieve and work with a subset of records that meet the defined filter conditions.
event.data
returns a promise resolving to an object with a records property so we extract the records property directly from the event.data object.
If rows are selected, only the corresponding records will be passed through the event for further processing.
//inside listener.on()
const { jobId } = event.context;
const { records } = await event.data;
try {
if (!records || records.length === 0) {
console.log("No rows were selected or in view.");
await api.jobs.fail(jobId, {
outcome: {
message: "No rows were selected or in view, please try again.",
},
});
return;
}
//print records
console.log(records);
await api.jobs.complete(jobId, {
outcome: {
message: "Records were printed to console, check it out.",
},
});
} catch (error) {
console.log(`Error: ${JSON.stringify(error, null, 2)}`);
await api.jobs.fail(jobId, {
outcome: {
message: "This action failed, check logs.",
},
});
}
For chosen records
When rows are selected, event.data
will only extract information exclusively for the chosen records, providing focused data retrieval for targeted analysis or operations.
event.data
returns a promise resolving to an object with a records property so we extract the records property directly from the event.data object.
//inside listener.on()
const { jobId } = event.context;
const { records } = await event.data;
try {
if (!records || records.length === 0) {
console.log("No rows were selected or in view.");
await api.jobs.fail(jobId, {
outcome: {
message: "No rows were selected or in view, please try again.",
},
});
return;
}
//print records
console.log(records);
await api.jobs.complete(jobId, {
outcome: {
message: "Records were printed to console, check it out.",
},
});
} catch (error) {
console.log(`Error: ${JSON.stringify(error, null, 2)}`);
await api.jobs.fail(jobId, {
outcome: {
message: "This action failed, check logs.",
},
});
}
File-mounted
File-mounted actions are represented as a dropdown menu for each file in the Files list. You can attach additional actions to a File.
Usage
First, listen for a file:ready
event and add one or more actions to the file.
listener.on("file:created", async ({ context: { fileId } }) => {
const file = await api.files.get(fileId);
const actions = file.data?.actions || [];
const newActions = [
...actions,
{
operation: "logFileContents",
label: "Log File Metadata",
description: "This will log the file metadata.",
},
{
operation: "decryptAction",
label: "Decrypt File",
description: "This will create a new decrypted file.",
},
];
await api.files.update(fileId, {
actions: newActions,
});
});
Next, listen for job:ready
and filter on the domain
(file) and the operation
of where the Action was placed. Be sure to complete to job when it’s complete.
//when the button is clicked in the file dropdown
listener.on(
"job:ready",
{ job: "file:logFileContents" },
async ({ context: { fileId, jobId } }) => {
await acknowledgeJob(jobId, "Gettin started.", 10);
const file = await api.files.get(fileId);
console.log({ file });
await completeJob(jobId, "Logging file contents is complete.");
}
);
listener.on(
"job:ready",
{ job: "file:decryptAction" },
async ({ context: { spaceId, fileId, jobId, environmentId } }) => {
try {
await acknowledgeJob(jobId, "Gettin started.", 10);
const fileResponse = await api.files.get(fileId);
const buffer = await getFileBufferFromApi(fileId);
const { name, ext } = fileResponse.data;
const newFileName = name
? name.split(".")[0] + "[Decrypted]." + ext
: "DecryptedFile.csv";
const formData = new FormData();
formData.append("file", buffer, { filename: newFileName });
formData.append("spaceId", spaceId);
formData.append("environmentId", environmentId);
await uploadDecryptedFile(formData);
await completeJob(jobId, "Decrypting is now complete.");
} catch (e) {
await failJob(jobId, "The decryption job failed.");
}
}
);
async function acknowledgeJob(jobId, info, progress, estimatedCompletionAt) {
await api.jobs.ack(jobId, {
info,
progress,
estimatedCompletionAt
});
}
async function completeJob(jobId, message) {
await api.jobs.complete(jobId, {
outcome: {
message,
},
});
}
async function failJob(jobId, message) {
await api.jobs.fail(jobId, {
outcome: {
message,
},
});
}
Get the example in the flatfile-docs-kitchen-sink ts or flatfile-docs-kitchen-sink js repo
Actions with Input Forms
If you configure input fields for your action, a secondary dialog will be presented to the end user, prompting them to provide the necessary information. Once the user has entered the required details, they can proceed with the action.
Usage
First, configure your action to have an inputForm on your Blueprint. These will appear once the action button is clicked.
actions: [
{
operation: "submitActionFg",
mode: "foreground",
label: "Submit data elsewhere",
type: "string",
description: "Submit this data to a webhook.",
primary: true,
inputForm: {
type: "simple",
fields: [
{
key: "priority",
label: "Priority level",
description: "Set the priority level.",
type: "enum",
config: {
options: [
{
value: "80ce8718a21c",
label: "High Priority",
description:
"Setting a value to High Priority means it will be prioritized over other values",
},
],
},
constraints: [
{
type: "required",
},
],
},
],
},
},
];
Next, listen for a job:ready
and filter on the job
you’d like to process. Grab the data entered in the form from the job itself and leverage it as required for your use case.
import api from "@flatfile/api";
import { responseRejectionHandler } from "@flatfile/util-response-rejection";
import axios from "axios";
export default function flatfileEventListener(listener) {
listener.on(
"job:ready",
{ job: "workbook:submitActionFg" },
async ({ context: { jobId, workbookId }, payload }) => {
const { data: workbook } = await api.workbooks.get(workbookId);
const { data: workbookSheets } = await api.sheets.list({ workbookId });
//get the input data by querying the job
const job = await api.jobs.get(jobId);
const priority = job.data.input["priority"];
const sheets = [];
for (const [_, element] of workbookSheets.entries()) {
const { data: records } = await api.records.get(element.id);
sheets.push({
...element,
...records,
});
}
try {
await api.jobs.ack(jobId, {
info: "Starting job to submit action to webhook.site",
progress: 10,
});
console.log(JSON.stringify(sheets, null, 2));
const webhookReceiver =
process.env.WEBHOOK_SITE_URL ||
"https://webhook.site/c83648d4-bf0c-4bb1-acb7-9c170dad4388"; //update this
const response = await axios.post(
webhookReceiver,
{
...payload,
method: "axios",
workbook: {
...workbook,
sheets,
priority,
},
},
{
headers: {
"Content-Type": "application/json",
},
}
);
if (response.status === 200) {
const rejections = response.data.rejections;
if (rejections) {
const totalRejectedRecords = await responseRejectionHandler(
rejections
);
return await api.jobs.complete(jobId, {
outcome: {
next: {
type: "id",
id: rejections.id,
label: "See rejections...",
},
message: `Data was submission was partially successful. ${totalRejectedRecords} record(s) were rejected.`,
},
});
}
return await api.jobs.complete(jobId, {
outcome: {
message:
"Data was successfully submitted to webhook.site. Go check it out at " +
webhookReceiver +
".",
},
});
} else {
throw new Error("Failed to submit data to webhook.site");
}
} catch (error) {
console.log(`webhook.site[error]: ${JSON.stringify(error, null, 2)}`);
await api.jobs.fail(jobId, {
outcome: {
message:
"This job failed probably because it couldn't find the webhook.site URL.",
},
});
}
}
);
}
// See full code example (https://github.com/FlatFilers/flatfile-docs-kitchen-sink/blob/main/javascript/shared/workbook_submit.js)
Example Project
Find the documents example in the Flatfile GitHub repository.