diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/RunTableAutomationsProcessStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/RunTableAutomationsProcessStep.java
new file mode 100644
index 00000000..29f1c2f1
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/RunTableAutomationsProcessStep.java
@@ -0,0 +1,152 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.processes.implementations.automation;
+
+
+import java.util.List;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.actions.automation.polling.PollingAutomationPerTableRunner;
+import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.automation.QAutomationProviderMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+
+
+/*******************************************************************************
+ ** Process to manually run table automations, for a table.
+ **
+ ** Useful, maybe, for an e2e test. Or, if you don't want jobs to be running,
+ ** but want to run automations by-hand, for some reason.
+ **
+ ** In the future, this class could take a param to only do inserts or updates.
+ **
+ ** Also, right now, only records that are Pending automations will be run -
+ ** again, that could be changed, presumably (take a list of records, always run, etc...)
+ *******************************************************************************/
+public class RunTableAutomationsProcessStep implements BackendStep, MetaDataProducerInterface
+{
+ public static final String NAME = "RunTableAutomationsProcess";
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public QProcessMetaData produce(QInstance qInstance) throws QException
+ {
+ QProcessMetaData processMetaData = new QProcessMetaData()
+ .withName(NAME)
+ .withStepList(List.of(
+ new QFrontendStepMetaData()
+ .withName("input")
+ .withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM))
+ .withFormField(new QFieldMetaData("tableName", QFieldType.STRING).withIsRequired(true))
+ .withFormField(new QFieldMetaData("automationProviderName", QFieldType.STRING)),
+ new QBackendStepMetaData()
+ .withName("run")
+ .withCode(new QCodeReference(getClass()))
+ .withInputData(new QFunctionInputMetaData()
+ .withField(new QFieldMetaData("tableName", QFieldType.STRING))
+ .withField(new QFieldMetaData("automationProviderName", QFieldType.STRING))),
+ new QFrontendStepMetaData()
+ .withName("output")
+ .withComponent(new QFrontendComponentMetaData().withType(QComponentType.VIEW_FORM))
+ .withFormField(new QFieldMetaData("ok", QFieldType.STRING))
+ ));
+
+ return (processMetaData);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
+ {
+ QInstance qInstance = QContext.getQInstance();
+
+ ////////////////////////////////////////////////////////////////////
+ // get tableName param (since this process is not table-specific) //
+ ////////////////////////////////////////////////////////////////////
+ String tableName = runBackendStepInput.getValueString("tableName");
+ if(!StringUtils.hasContent(tableName))
+ {
+ throw (new QException("Missing required input value: tableName"));
+ }
+
+ if(!QContext.getQInstance().getTables().containsKey(tableName))
+ {
+ throw (new QException("Unrecognized table name: " + tableName));
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ // get the automation provider name to use - either as the only-one-in-instance, or via param //
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ String automationProviderName = runBackendStepInput.getValueString("automationProviderName");
+ if(!StringUtils.hasContent(automationProviderName))
+ {
+ Map automationProviders = CollectionUtils.nonNullMap(qInstance.getAutomationProviders());
+ if(automationProviders.size() == 1)
+ {
+ automationProviderName = automationProviders.keySet().iterator().next();
+ }
+ else
+ {
+ throw (new QException("Missing required input value: automationProviderName (and there is not exactly 1 in the active instance)"));
+ }
+ }
+
+ /////////////////////////////////////////////
+ // run automations for the requested table //
+ /////////////////////////////////////////////
+ List tableActions = PollingAutomationPerTableRunner.getTableActions(qInstance, automationProviderName);
+ for(PollingAutomationPerTableRunner.TableActionsInterface tableAction : tableActions)
+ {
+ if(tableName.equals(tableAction.tableName()))
+ {
+ PollingAutomationPerTableRunner pollingAutomationPerTableRunner = new PollingAutomationPerTableRunner(qInstance, automationProviderName, () -> QContext.getQSession(), tableAction);
+ pollingAutomationPerTableRunner.processTableInsertOrUpdate(qInstance.getTable(tableAction.tableName()), tableAction.status());
+ }
+ }
+
+ runBackendStepOutput.addValue("ok", "true");
+ }
+
+}
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/RunTableAutomationsProcessStepTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/RunTableAutomationsProcessStepTest.java
new file mode 100644
index 00000000..0a5bd9f0
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/automation/RunTableAutomationsProcessStepTest.java
@@ -0,0 +1,97 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.processes.implementations.automation;
+
+
+import com.kingsrook.qqq.backend.core.BaseTest;
+import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
+import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
+import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.utils.TestUtils;
+import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeSupplier;
+import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+
+/*******************************************************************************
+ ** Unit test for RunTableAutomationsProcessStep
+ *******************************************************************************/
+class RunTableAutomationsProcessStepTest extends BaseTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void test() throws Exception
+ {
+ UnsafeSupplier getAutomationStatus = () -> new GetAction().executeForRecord(new GetInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withPrimaryKey(1)).getValueInteger("qqqAutomationStatus");
+
+ new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withRecord(new QRecord()));
+ assertEquals(AutomationStatus.PENDING_INSERT_AUTOMATIONS.getId(), getAutomationStatus.get());
+
+ RunBackendStepInput input = new RunBackendStepInput();
+ input.addValue("tableName", TestUtils.TABLE_NAME_PERSON_MEMORY);
+ RunBackendStepOutput output = new RunBackendStepOutput();
+ new RunTableAutomationsProcessStep().run(input, output);
+ assertEquals("true", output.getValue("ok"));
+
+ assertEquals(AutomationStatus.OK.getId(), getAutomationStatus.get());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testThrowsWithoutTableName() throws QException
+ {
+ RunBackendStepInput input = new RunBackendStepInput();
+ RunBackendStepOutput output = new RunBackendStepOutput();
+ assertThatThrownBy(() -> new RunTableAutomationsProcessStep().run(input, output))
+ .hasMessageContaining("Missing required input value: tableName");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testThrowsWithInvalidTableName() throws QException
+ {
+ RunBackendStepInput input = new RunBackendStepInput();
+ RunBackendStepOutput output = new RunBackendStepOutput();
+ input.addValue("tableName", "asdf");
+ assertThatThrownBy(() -> new RunTableAutomationsProcessStep().run(input, output))
+ .hasMessageContaining("Unrecognized table name: asdf");
+ }
+
+}
\ No newline at end of file