The DevXP engineering team hosts office hours every Thursday at 11 a.m.
Pacific Time where we answer your questions live and help you get up and
running with Flatfile. Join
us!
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.
Once a user has extracted and mapped data into a , 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.
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).
//your workbook should have an action that looks like thisactions:[{operation:'submitActionFg',mode:'foreground',label:'Submit data elsewhere',type:'string',description:'Submit this data to a webhook.',primary:true,},{...}],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.
red.on("job:ready",{job:"workbook:submitAction"},async(event:FlatfileEvent)=>{const{ jobId, workbookId }= event.context;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,});// TODO: place your webhook url hereconst webhookReceiver ="https://webhook.site/...";const response =awaitfetch(webhookReceiver,{method:"POST",headers:{"Content-Type":"application/json",},body:JSON.stringify({workbook:{...workbook, sheets,},}),});if(response.status===200){const responseData =await response.json();const rejections = responseData.rejections;if(rejections){const outcome =awaitresponseRejectionHandler(rejections);await api.jobs.complete(jobId, outcome);}await api.jobs.complete(jobId,{outcome:{message:`Data was successfully submitted to webhook.site. Go check it out at ${webhookReceiver}.`,},});}else{thrownewError("Failed to submit data to webhook.site");}return;}catch(error){console.error(error);await api.jobs.fail(jobId,{outcome:{message:"This job failed probably because it couldn't find the webhook.site URL.",},});return;}});// See full code example (https://github.com/FlatFilers/flatfile-docs-kitchen-sink/blob/main/javascript/shared/workbook_submit.js)
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.
importapifrom"@flatfile/api";exportdefaultfunctionflatfileEventListener(listener){ listener.on("file:created",async({context:{ spaceId, fileId }})=>{const fileName =(await api.files.get(fileId)).data.name;const bodyText ="# Welcome\n"+"### Say hello to your first customer Space in the new Flatfile!\n"+"Let's begin by first getting acquainted with what you're seeing in your Space initially.\n"+"---\n"+"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,},],});});}
In your listener, listen for the job’s event and perform your desired operations.
exportdefaultfunctionflatfileEventListener(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" value must be a whole integerprogress:10,estimatedCompletionAt:newDate("Tue Aug 23 2023 16:19:42 GMT-0700"),});// Do your work hereawait 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.`,},});}});}
sheets:[{name:"Sheet Name",actions:[{operation:'duplicate',mode:'background',label:'Duplicate selected names',description:'Duplicate names for selected rows',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" value must be a whole integerprogress:10,estimatedCompletionAt:newDate("Tue Aug 23 2023 16:19:42 GMT-0700"),});// Do your work hereawait 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.",});}});
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:
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 sheetconst response =await api.records.get(sheetId);//print recordsconsole.log(response.data.records);
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 recordsconsole.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.",},});}
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.
This code is the same as the filtered view of the Sheet.
//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 recordsconsole.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.",},});}
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.
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.
First, configure your action to have an inputForm on your Blueprint. These will appear once the action button is clicked.
workbook.js
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",defaultValue:"80ce8718a21c",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.
importapifrom"@flatfile/api";exportdefaultasyncfunction(listener){ listener.on("job:ready",{job:"workbook:actionWithInput"},async(event)=>{const{ jobId }= event.context;try{await api.jobs.ack(jobId,{info:"Acknowledging job",progress:1,});// retrieve inputconst{data: job }=await api.jobs.get(jobId);const input = job.input;console.log({ input });// do something with input...await api.jobs.complete(jobId,{outcome:{message:"Action was successful",},});return;}catch(error){console.error(error);await api.jobs.fail(jobId,{outcome:{message:"Action failed",},});return;}});}
Adding a hasAllValid constraint on an Action will
disable a Workbook Action when there are invalid records.
Adding a hasData on an Action will
disable a Workbook Action when there are no records.
actions:[{operation:'submitActionFg',mode:'foreground',label:'Submit data elsewhere',description:'Submit this data to a webhook.',primary:true,constraints:[{type:'hasAllValid'},{type:'hasData'}]},{...}],
Add custom messages to actions, tailored according to their state:
Error
Info
These messages will be displayed as tooltips when users hover over an action, providing context-specific text that corresponds to the action’s current state. When an error message is present on an action, the action will be disabled.
Simply add a messages property to your action configuration. This property should be an array of objects, each specifying a message type and its content.
actions:[{operation:'duplicate',mode:'background',label:'Duplicate selected names',description:'Duplicate names for selected rows',messages:[{type:'error',content:'This is an error message'},],primary:true,},{...}]