Automation trigger filtering (#14123)

* backend for triggering automation based on filters

* frontend for handling triggering automations on filter / old row

* lint and bug fix

* fix issue with test header

* make test data optional

* improve safety on trigger gate

* add support for running trigger with filter if no change happened but filter matches

* update var naming to actually make sense

* tests

* fix lint

* improve gating for shouldTrigger check

* remove unecessary cast

* unecessary tableId check

* frontend text updates

* resolving comments

* pro

* Update packages/types/src/documents/app/automation.ts

Co-authored-by: Sam Rose <hello@samwho.dev>

* link out to docs for trigger filtering

* fix pro

* more pr comments

* use getAppId

---------

Co-authored-by: Sam Rose <hello@samwho.dev>
This commit is contained in:
Peter Clement 2024-07-18 10:38:15 +01:00 committed by GitHub
parent 5e3bec86ed
commit 7fd55fe27d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 364 additions and 66 deletions

View File

@ -16,13 +16,12 @@
export let enableNaming = true export let enableNaming = true
let validRegex = /^[A-Za-z0-9_\s]+$/ let validRegex = /^[A-Za-z0-9_\s]+$/
let typing = false let typing = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
$: stepNames = $selectedAutomation?.definition.stepNames $: stepNames = $selectedAutomation?.definition.stepNames
$: automationName = stepNames?.[block.id] || block?.name || "" $: automationName = stepNames?.[block.id] || block?.name || ""
$: automationNameError = getAutomationNameError(automationName) $: automationNameError = getAutomationNameError(automationName)
$: status = updateStatus(testResult, isTrigger) $: status = updateStatus(testResult)
$: isHeaderTrigger = isTrigger || block.type === "TRIGGER" $: isHeaderTrigger = isTrigger || block.type === "TRIGGER"
$: { $: {
@ -43,7 +42,7 @@
}) })
} }
function updateStatus(results, isTrigger) { function updateStatus(results) {
if (!results) { if (!results) {
return {} return {}
} }
@ -56,7 +55,6 @@
return { negative: true, message: "Error" } return { negative: true, message: "Error" }
} }
} }
const getAutomationNameError = name => { const getAutomationNameError = name => {
if (stepNames) { if (stepNames) {
for (const [key, value] of Object.entries(stepNames)) { for (const [key, value] of Object.entries(stepNames)) {

View File

@ -12,14 +12,31 @@
let blocks let blocks
function prepTestResults(results) { function prepTestResults(results) {
return results?.steps.filter(x => x.stepId !== ActionStepID.LOOP || []) if (results.message) {
return [
{
inputs: {},
outputs: {
success: results.outputs?.success || false,
status: results.outputs?.status || "unknown",
message: results.message,
},
},
]
} else {
return results?.steps?.filter(x => x.stepId !== ActionStepID.LOOP) || []
}
} }
$: filteredResults = prepTestResults(testResults) $: filteredResults = prepTestResults(testResults)
$: { $: {
blocks = [] if (testResults.message) {
if (automation) { blocks = automation?.definition?.trigger
? [automation.definition.trigger]
: []
} else if (automation) {
blocks = []
if (automation.definition.trigger) { if (automation.definition.trigger) {
blocks.push(automation.definition.trigger) blocks.push(automation.definition.trigger)
} }
@ -46,7 +63,9 @@
open={!!openBlocks[block.id]} open={!!openBlocks[block.id]}
on:toggle={() => (openBlocks[block.id] = !openBlocks[block.id])} on:toggle={() => (openBlocks[block.id] = !openBlocks[block.id])}
isTrigger={idx === 0} isTrigger={idx === 0}
testResult={filteredResults?.[idx]} testResult={testResults.message
? testResults
: filteredResults?.[idx]}
showTestStatus showTestStatus
{block} {block}
{idx} {idx}
@ -68,7 +87,9 @@
<Tabs quiet noHorizPadding selected="Input"> <Tabs quiet noHorizPadding selected="Input">
<Tab title="Input"> <Tab title="Input">
<div class="wrap"> <div class="wrap">
{#if filteredResults?.[idx]?.inputs} {#if testResults.message}
No input
{:else if filteredResults?.[idx]?.inputs}
<JsonView depth={2} json={filteredResults?.[idx]?.inputs} /> <JsonView depth={2} json={filteredResults?.[idx]?.inputs} />
{:else} {:else}
No input No input
@ -77,13 +98,22 @@
</Tab> </Tab>
<Tab title="Output"> <Tab title="Output">
<div class="wrap"> <div class="wrap">
{#if filteredResults?.[idx]?.outputs} {#if testResults.message}
<JsonView
depth={2}
json={{
success: testResults.outputs?.success || false,
status: testResults.outputs?.status || "unknown",
message: testResults.message,
}}
/>
{:else if filteredResults?.[idx]?.outputs}
<JsonView <JsonView
depth={2} depth={2}
json={filteredResults?.[idx]?.outputs} json={filteredResults?.[idx]?.outputs}
/> />
{:else} {:else}
No input No output
{/if} {/if}
</div> </div>
</Tab> </Tab>

View File

@ -17,7 +17,9 @@
Helpers, Helpers,
Toggle, Toggle,
Divider, Divider,
Icon,
} from "@budibase/bbui" } from "@budibase/bbui"
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte" import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
import { automationStore, selectedAutomation, tables } from "stores/builder" import { automationStore, selectedAutomation, tables } from "stores/builder"
import { environment, licensing } from "stores/portal" import { environment, licensing } from "stores/portal"
@ -365,41 +367,74 @@
/** /**
* Handler for row trigger automation updates. * Handler for row trigger automation updates.
@param {object} update - An automation block.inputs update object * @param {object} update - An automation block.inputs update object
@example * @param {string} [update.tableId] - The ID of the table
onRowTriggerUpdate({ * @param {object} [update.filters] - Filter configuration for the row trigger
"tableId" : "ta_bb_employee" * @param {object} [update.filters-def] - Filter definitions for the row trigger
}) * @example
* // Example with tableId
* onRowTriggerUpdate({
* "tableId" : "ta_bb_employee"
* })
* @example
* // Example with filters
* onRowTriggerUpdate({
* filters: {
* equal: { "1:Approved": "true" }
* },
* "filters-def": [{
* id: "oH1T4S49n",
* field: "1:Approved",
* operator: "equal",
* value: "true",
* valueType: "Value",
* type: "string"
* }]
* })
*/ */
const onRowTriggerUpdate = async update => { const onRowTriggerUpdate = async update => {
if ( if (
Object.hasOwn(update, "tableId") && ["tableId", "filters", "meta"].some(key => Object.hasOwn(update, key))
$selectedAutomation.testData?.row?.tableId !== update.tableId
) { ) {
try { try {
const reqSchema = getSchemaForDatasourcePlus(update.tableId, { let updatedAutomation
searchableSchema: true,
}).schema
// Parse the block inputs as usual if (
const updatedAutomation = Object.hasOwn(update, "tableId") &&
await automationStore.actions.processBlockInputs(block, { $selectedAutomation.testData?.row?.tableId !== update.tableId
schema: reqSchema, ) {
...update, const reqSchema = getSchemaForDatasourcePlus(update.tableId, {
}) searchableSchema: true,
}).schema
// Save the entire automation and reset the testData updatedAutomation = await automationStore.actions.processBlockInputs(
await automationStore.actions.save({ block,
...updatedAutomation, {
testData: { schema: reqSchema,
// Reset Core fields ...update,
row: { tableId: update.tableId }, }
oldRow: { tableId: update.tableId }, )
meta: {},
id: "", // Reset testData when tableId changes
revision: "", updatedAutomation = {
}, ...updatedAutomation,
}) testData: {
row: { tableId: update.tableId },
oldRow: { tableId: update.tableId },
meta: {},
id: "",
revision: "",
},
}
} else {
// For filters update, just process block inputs without resetting testData
updatedAutomation = await automationStore.actions.processBlockInputs(
block,
update
)
}
await automationStore.actions.save(updatedAutomation)
return return
} catch (e) { } catch (e) {
@ -408,7 +443,6 @@
} }
} }
} }
/** /**
* Handler for App trigger automation updates. * Handler for App trigger automation updates.
* Ensure updates to the field list are reflected in testData * Ensure updates to the field list are reflected in testData
@ -743,6 +777,7 @@
value.customType !== "triggerSchema" && value.customType !== "triggerSchema" &&
value.customType !== "automationFields" && value.customType !== "automationFields" &&
value.customType !== "fields" && value.customType !== "fields" &&
value.customType !== "trigger_filter_setting" &&
value.type !== "signature_single" && value.type !== "signature_single" &&
value.type !== "attachment" && value.type !== "attachment" &&
value.type !== "attachment_single" value.type !== "attachment_single"
@ -807,13 +842,23 @@
{@const label = getFieldLabel(key, value)} {@const label = getFieldLabel(key, value)}
<div class:block-field={shouldRenderField(value)}> <div class:block-field={shouldRenderField(value)}>
{#if key !== "fields" && value.type !== "boolean" && shouldRenderField(value)} {#if key !== "fields" && value.type !== "boolean" && shouldRenderField(value)}
<Label <div class="label-container">
tooltip={value.title === "Binding / Value" <Label>
? "If using the String input type, please use a comma or newline separated string" {label}
: null} </Label>
> {#if value.customType === "trigger_filter"}
{label} <Icon
</Label> hoverable
on:click={() =>
window.open(
"https://docs.budibase.com/docs/row-trigger-filters",
"_blank"
)}
size="XS"
name="InfoOutline"
/>
{/if}
</div>
{/if} {/if}
<div class:field-width={shouldRenderField(value)}> <div class:field-width={shouldRenderField(value)}>
{#if value.type === "string" && value.enum && canShowField(key, value)} {#if value.type === "string" && value.enum && canShowField(key, value)}
@ -932,8 +977,12 @@
{/if} {/if}
</div> </div>
</div> </div>
{:else if value.customType === "filters"} {:else if value.customType === "filters" || value.customType === "trigger_filter"}
<ActionButton on:click={drawer.show}>Define filters</ActionButton> <ActionButton fullWidth on:click={drawer.show}
>{filters.length > 0
? "Update Filter"
: "No Filter set"}</ActionButton
>
<Drawer bind:this={drawer} title="Filtering"> <Drawer bind:this={drawer} title="Filtering">
<Button cta slot="buttons" on:click={() => saveFilters(key)}> <Button cta slot="buttons" on:click={() => saveFilters(key)}>
Save Save
@ -945,6 +994,7 @@
{schemaFields} {schemaFields}
datasource={{ type: "table", tableId }} datasource={{ type: "table", tableId }}
panel={AutomationBindingPanel} panel={AutomationBindingPanel}
showFilterEmptyDropdown={!rowTriggers.includes(stepId)}
on:change={e => (tempFilters = e.detail)} on:change={e => (tempFilters = e.detail)}
/> />
</DrawerContent> </DrawerContent>
@ -1085,6 +1135,11 @@
{/if} {/if}
<style> <style>
.label-container {
display: flex;
align-items: center;
gap: var(--spacing-s);
}
.field-width { .field-width {
width: 320px; width: 320px;
} }

View File

@ -14,7 +14,7 @@
export let panel = ClientBindingPanel export let panel = ClientBindingPanel
export let allowBindings = true export let allowBindings = true
export let datasource export let datasource
export let showFilterEmptyDropdown
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let rawFilters let rawFilters
@ -63,6 +63,7 @@
{schemaFields} {schemaFields}
{datasource} {datasource}
{allowBindings} {allowBindings}
{showFilterEmptyDropdown}
> >
<div slot="filtering-hero-content" /> <div slot="filtering-hero-content" />

View File

@ -208,7 +208,7 @@ const automationActions = store => ({
const message = err.message || err.status || JSON.stringify(err) const message = err.message || err.status || JSON.stringify(err)
throw `Automation test failed - ${message}` throw `Automation test failed - ${message}`
} }
if (!result?.trigger && !result?.steps?.length) { if (!result?.trigger && !result?.steps?.length && !result?.message) {
if (result?.err?.code === "usage_limit_exceeded") { if (result?.err?.code === "usage_limit_exceeded") {
throw "You have exceeded your automation quota" throw "You have exceeded your automation quota"
} }

View File

@ -26,7 +26,7 @@
export let behaviourFilters = false export let behaviourFilters = false
export let allowBindings = false export let allowBindings = false
export let filtersLabel = "Filters" export let filtersLabel = "Filters"
export let showFilterEmptyDropdown = true
$: { $: {
if ( if (
tables.find( tables.find(
@ -218,7 +218,7 @@
on:change={e => handleAllOr(e.detail)} on:change={e => handleAllOr(e.detail)}
placeholder={null} placeholder={null}
/> />
{#if datasource?.type === "table"} {#if datasource?.type === "table" && showFilterEmptyDropdown}
<Select <Select
label="When filter empty" label="When filter empty"
value={onEmptyFilter} value={onEmptyFilter}

View File

@ -11,7 +11,7 @@ import {
} from "../../../automations" } from "../../../automations"
import { events } from "@budibase/backend-core" import { events } from "@budibase/backend-core"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import { Automation } from "@budibase/types" import { Automation, FieldType, Table } from "@budibase/types"
import { mocks } from "@budibase/backend-core/tests" import { mocks } from "@budibase/backend-core/tests"
import { FilterConditions } from "../../../automations/steps/filter" import { FilterConditions } from "../../../automations/steps/filter"
@ -23,6 +23,7 @@ let {
automationStep, automationStep,
collectAutomation, collectAutomation,
filterAutomation, filterAutomation,
updateRowAutomationWithFilters,
} = setup.structures } = setup.structures
describe("/automations", () => { describe("/automations", () => {
@ -452,14 +453,13 @@ describe("/automations", () => {
let table = await config.createTable() let table = await config.createTable()
let automation = await filterAutomation() let automation = await filterAutomation(config.getAppId())
automation.definition.trigger.inputs.tableId = table._id automation.definition.trigger.inputs.tableId = table._id
automation.definition.steps[0].inputs = { automation.definition.steps[0].inputs = {
condition: FilterConditions.EQUAL, condition: FilterConditions.EQUAL,
field: "{{ trigger.row.City }}", field: "{{ trigger.row.City }}",
value: "{{ trigger.oldRow.City }}", value: "{{ trigger.oldRow.City }}",
} }
automation.appId = config.appId!
automation = await config.createAutomation(automation) automation = await config.createAutomation(automation)
let triggerInputs = { let triggerInputs = {
oldRow: { oldRow: {
@ -474,4 +474,91 @@ describe("/automations", () => {
} }
) )
}) })
describe("Automation Update / Creator row trigger filtering", () => {
let table: Table
beforeAll(async () => {
table = await config.createTable({
name: "table",
type: "table",
schema: {
Approved: {
name: "Approved",
type: FieldType.BOOLEAN,
},
},
})
})
const testCases = [
{
description: "should run when Approved changes from false to true",
filters: {
equal: { "1:Approved": true },
},
row: { Approved: "true" },
oldRow: { Approved: "false" },
expectToRun: true,
},
{
description: "should run when Approved is true in both old and new row",
filters: { equal: { "1:Approved": true } },
row: { Approved: "true" },
oldRow: { Approved: "true" },
expectToRun: true,
},
{
description:
"should run when a contains filter matches the correct options",
filters: {
contains: { "1:opts": ["Option 1", "Option 3"] },
},
row: { opts: ["Option 1", "Option 3"] },
oldRow: { opts: ["Option 3"] },
expectToRun: true,
},
{
description:
"should not run when opts doesn't contain any specified option",
filters: {
contains: { "1:opts": ["Option 1", "Option 2"] },
},
row: { opts: ["Option 3", "Option 4"] },
oldRow: { opts: ["Option 3", "Option 4"] },
expectToRun: false,
},
]
it.each(testCases)(
"$description",
async ({ filters, row, oldRow, expectToRun }) => {
let automation = await updateRowAutomationWithFilters(config.getAppId())
automation.definition.trigger.inputs = {
tableId: table._id,
filters,
}
automation = await config.createAutomation(automation)
const inputs = {
row: {
tableId: table._id,
...row,
},
oldRow: {
tableId: table._id,
...oldRow,
},
}
const res = await testAutomation(config, automation, inputs)
if (expectToRun) {
expect(res.body.steps[1].outputs.success).toEqual(true)
} else {
expect(res.body.outputs.success).toEqual(false)
}
}
)
})
}) })

View File

@ -23,6 +23,11 @@ export const definition: AutomationTriggerSchema = {
customType: AutomationCustomIOType.TABLE, customType: AutomationCustomIOType.TABLE,
title: "Table", title: "Table",
}, },
filters: {
type: AutomationIOType.OBJECT,
customType: AutomationCustomIOType.FILTERS,
title: "Filtering",
},
}, },
required: ["tableId"], required: ["tableId"],
}, },

View File

@ -23,6 +23,11 @@ export const definition: AutomationTriggerSchema = {
customType: AutomationCustomIOType.TABLE, customType: AutomationCustomIOType.TABLE,
title: "Table", title: "Table",
}, },
filters: {
type: AutomationIOType.OBJECT,
customType: AutomationCustomIOType.TRIGGER_FILTER,
title: "Filtering",
},
}, },
required: ["tableId"], required: ["tableId"],
}, },

View File

@ -15,14 +15,19 @@ import {
AutomationJob, AutomationJob,
AutomationEventType, AutomationEventType,
UpdatedRowEventEmitter, UpdatedRowEventEmitter,
SearchFilters,
AutomationStoppedReason,
AutomationStatus,
} from "@budibase/types" } from "@budibase/types"
import { executeInThread } from "../threads/automation" import { executeInThread } from "../threads/automation"
import { dataFilters } from "@budibase/shared-core"
export const TRIGGER_DEFINITIONS = definitions export const TRIGGER_DEFINITIONS = definitions
const JOB_OPTS = { const JOB_OPTS = {
removeOnComplete: true, removeOnComplete: true,
removeOnFail: true, removeOnFail: true,
} }
import * as automationUtils from "../automations/automationUtils"
async function getAllAutomations() { async function getAllAutomations() {
const db = context.getAppDB() const db = context.getAppDB()
@ -33,7 +38,7 @@ async function getAllAutomations() {
} }
async function queueRelevantRowAutomations( async function queueRelevantRowAutomations(
event: { appId: string; row: Row }, event: { appId: string; row: Row; oldRow: Row },
eventType: string eventType: string
) { ) {
if (event.appId == null) { if (event.appId == null) {
@ -62,9 +67,15 @@ async function queueRelevantRowAutomations(
) { ) {
continue continue
} }
const shouldTrigger = await checkTriggerFilters(automation, {
row: event.row,
oldRow: event.oldRow,
})
if ( if (
automationTrigger?.inputs && automationTrigger?.inputs &&
automationTrigger.inputs.tableId === event.row.tableId automationTrigger.inputs.tableId === event.row.tableId &&
shouldTrigger
) { ) {
try { try {
await automationQueue.add({ automation, event }, JOB_OPTS) await automationQueue.add({ automation, event }, JOB_OPTS)
@ -103,6 +114,11 @@ emitter.on(AutomationEventType.ROW_DELETE, async function (event) {
await queueRelevantRowAutomations(event, AutomationEventType.ROW_DELETE) await queueRelevantRowAutomations(event, AutomationEventType.ROW_DELETE)
}) })
function rowPassesFilters(row: Row, filters: SearchFilters) {
const filteredRows = dataFilters.runQuery([row], filters)
return filteredRows.length > 0
}
export async function externalTrigger( export async function externalTrigger(
automation: Automation, automation: Automation,
params: { fields: Record<string, any>; timeout?: number }, params: { fields: Record<string, any>; timeout?: number },
@ -126,7 +142,23 @@ export async function externalTrigger(
} }
params.fields = coercedFields params.fields = coercedFields
} }
const data: AutomationData = { automation, event: params as any } const data: AutomationData = { automation, event: params }
const shouldTrigger = await checkTriggerFilters(automation, {
row: data.event?.row ?? {},
oldRow: data.event?.oldRow ?? {},
})
if (!shouldTrigger) {
return {
outputs: {
success: false,
status: AutomationStatus.STOPPED,
},
message: AutomationStoppedReason.TRIGGER_FILTER_NOT_MET,
}
}
if (getResponses) { if (getResponses) {
data.event = { data.event = {
...data.event, ...data.event,
@ -171,3 +203,25 @@ export async function rebootTrigger() {
}) })
} }
} }
async function checkTriggerFilters(
automation: Automation,
event: { row: Row; oldRow: Row }
): Promise<boolean> {
const trigger = automation.definition.trigger
const filters = trigger?.inputs?.filters
const tableId = trigger?.inputs?.tableId
if (!filters) {
return true
}
if (
trigger.stepId === definitions.ROW_UPDATED.stepId ||
trigger.stepId === definitions.ROW_SAVED.stepId
) {
const newRow = await automationUtils.cleanUpRow(tableId, event.row)
return rowPassesFilters(newRow, filters)
}
return true
}

View File

@ -357,18 +357,23 @@ export function collectAutomation(tableId?: string): Automation {
}, },
}, },
} }
return automation as Automation return automation
} }
export function filterAutomation(tableId?: string): Automation { export function filterAutomation(appId: string, tableId?: string): Automation {
const automation: any = { const automation: Automation = {
name: "looping", name: "looping",
type: "automation", type: "automation",
appId,
definition: { definition: {
steps: [ steps: [
{ {
name: "Filter Step",
tagline: "An automation filter step",
description: "A filter automation",
id: "b", id: "b",
type: "ACTION", icon: "Icon",
type: AutomationStepType.ACTION,
internal: true, internal: true,
stepId: AutomationActionStepId.FILTER, stepId: AutomationActionStepId.FILTER,
inputs: {}, inputs: {},
@ -376,8 +381,12 @@ export function filterAutomation(tableId?: string): Automation {
}, },
], ],
trigger: { trigger: {
name: "trigger Step",
tagline: "An automation trigger",
description: "A trigger",
icon: "Icon",
id: "a", id: "a",
type: "TRIGGER", type: AutomationStepType.TRIGGER,
event: "row:save", event: "row:save",
stepId: AutomationTriggerStepId.ROW_SAVED, stepId: AutomationTriggerStepId.ROW_SAVED,
inputs: { inputs: {
@ -387,7 +396,45 @@ export function filterAutomation(tableId?: string): Automation {
}, },
}, },
} }
return automation as Automation return automation
}
export function updateRowAutomationWithFilters(appId: string): Automation {
const automation: Automation = {
name: "updateRowWithFilters",
type: "automation",
appId,
definition: {
steps: [
{
name: "Filter Step",
tagline: "An automation filter step",
description: "A filter automation",
icon: "Icon",
id: "b",
type: AutomationStepType.ACTION,
internal: true,
stepId: AutomationActionStepId.SERVER_LOG,
inputs: {},
schema: BUILTIN_ACTION_DEFINITIONS.SERVER_LOG.schema,
},
],
trigger: {
name: "trigger Step",
tagline: "An automation trigger",
description: "A trigger",
icon: "Icon",
id: "a",
type: AutomationStepType.TRIGGER,
event: "row:update",
stepId: AutomationTriggerStepId.ROW_UPDATED,
inputs: {},
schema: TRIGGER_DEFINITIONS.ROW_UPDATED.schema,
},
},
}
return automation
} }
export function basicAutomationResults( export function basicAutomationResults(

View File

@ -35,6 +35,7 @@ export enum AutomationCustomIOType {
AUTOMATION = "automation", AUTOMATION = "automation",
AUTOMATION_FIELDS = "automationFields", AUTOMATION_FIELDS = "automationFields",
MULTI_ATTACHMENTS = "multi_attachments", MULTI_ATTACHMENTS = "multi_attachments",
TRIGGER_FILTER = "trigger_filter",
} }
export enum AutomationTriggerStepId { export enum AutomationTriggerStepId {
@ -128,6 +129,15 @@ export interface Automation extends Document {
internal?: boolean internal?: boolean
type?: string type?: string
disabled?: boolean disabled?: boolean
testData?: {
row?: Row
meta: {
[key: string]: unknown
}
id: string
revision: string
oldRow?: Row
}
} }
interface BaseIOStructure { interface BaseIOStructure {
@ -201,6 +211,10 @@ export enum AutomationStatus {
STOPPED_ERROR = "stopped_error", STOPPED_ERROR = "stopped_error",
} }
export enum AutomationStoppedReason {
TRIGGER_FILTER_NOT_MET = "Automation did not run. Filter conditions in trigger were not met.",
}
export interface AutomationResults { export interface AutomationResults {
automationId?: string automationId?: string
status?: AutomationStatus status?: AutomationStatus

View File

@ -1,4 +1,4 @@
import { Automation, AutomationMetadata } from "../../documents" import { Automation, AutomationMetadata, Row } from "../../documents"
import { Job } from "bull" import { Job } from "bull"
export interface AutomationDataEvent { export interface AutomationDataEvent {
@ -6,6 +6,8 @@ export interface AutomationDataEvent {
metadata?: AutomationMetadata metadata?: AutomationMetadata
automation?: Automation automation?: Automation
timeout?: number timeout?: number
row?: Row
oldRow?: Row
} }
export interface AutomationData { export interface AutomationData {