diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/ReportValuesDynamicFormWidgetRenderer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/ReportValuesDynamicFormWidgetRenderer.java
new file mode 100644
index 00000000..179bb413
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/ReportValuesDynamicFormWidgetRenderer.java
@@ -0,0 +1,175 @@
+/*
+ * 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.model.savedreports;
+
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
+import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
+import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
+import com.kingsrook.qqq.backend.core.model.dashboard.widgets.DynamicFormWidgetData;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.processes.implementations.savedreports.SavedReportToReportMetaDataAdapter;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.backend.core.utils.JsonUtils;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import com.kingsrook.qqq.backend.core.utils.ValueUtils;
+import org.json.JSONObject;
+import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
+
+
+/*******************************************************************************
+ ** Note - exists under 2 names, for the RenderSavedReport process, and for the
+ ** ScheduledReport table
+ *******************************************************************************/
+public class ReportValuesDynamicFormWidgetRenderer extends AbstractWidgetRenderer
+{
+ private static final QLogger LOG = QLogger.getLogger(ReportValuesDynamicFormWidgetRenderer.class);
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public RenderWidgetOutput render(RenderWidgetInput input) throws QException
+ {
+ try
+ {
+ List fieldList = new ArrayList<>();
+ Map defaultValues = new HashMap<>();
+
+ //////////////////////////////////////////////////////////////////////////////
+ // read params to ultimately find the query filter that has variables in it //
+ //////////////////////////////////////////////////////////////////////////////
+ SavedReport savedReport = null;
+ if(input.getQueryParams().containsKey("savedReportId"))
+ {
+ QRecord record = new GetAction().executeForRecord(new GetInput(SavedReport.TABLE_NAME).withPrimaryKey(ValueUtils.getValueAsInteger(input.getQueryParams().get("savedReportId"))));
+ savedReport = new SavedReport(record);
+ }
+ else if(input.getQueryParams().containsKey("id"))
+ {
+ QRecord scheduledReportRecord = new GetAction().executeForRecord(new GetInput(ScheduledReport.TABLE_NAME).withPrimaryKey(ValueUtils.getValueAsInteger(input.getQueryParams().get("id"))));
+ QRecord record = new GetAction().executeForRecord(new GetInput(SavedReport.TABLE_NAME).withPrimaryKey(ValueUtils.getValueAsInteger(scheduledReportRecord.getValueInteger("savedReportId"))));
+ savedReport = new SavedReport(record);
+
+ String inputValues = scheduledReportRecord.getValueString("inputValues");
+ if(StringUtils.hasContent(inputValues))
+ {
+ JSONObject jsonObject = JsonUtils.toJSONObject(inputValues);
+ for(String key : jsonObject.keySet())
+ {
+ defaultValues.put(key, jsonObject.optString(key));
+ }
+ }
+ }
+ else
+ {
+ //////////////////////////////////
+ // return quietly w/ nothing... //
+ //////////////////////////////////
+ DynamicFormWidgetData widgetData = new DynamicFormWidgetData();
+ return new RenderWidgetOutput(widgetData);
+ }
+
+ if(StringUtils.hasContent(savedReport.getQueryFilterJson()))
+ {
+ QQueryFilter queryFilter = SavedReportToReportMetaDataAdapter.getQQueryFilter(savedReport.getQueryFilterJson());
+ QTableMetaData table = QContext.getQInstance().getTable(savedReport.getTableName());
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // find variables in the query filter; convert them to a list of fields for the dynamic form //
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ for(QFilterCriteria criteria : CollectionUtils.nonNullList(queryFilter.getCriteria()))
+ {
+ /////////////////////////////////
+ // todo - only variable fields //
+ /////////////////////////////////
+
+ ////////////////////////////////
+ // todo - twice for "between" //
+ ////////////////////////////////
+
+ //////////////////////////
+ // todo - join fields!! //
+ //////////////////////////
+ QFieldMetaData fieldMetaData = table.getField(criteria.getFieldName()).clone();
+
+ /////////////////////////////////
+ // make name & label for field //
+ /////////////////////////////////
+ String operatorHumanish = StringUtils.allCapsToMixedCase(criteria.getOperator().name()); // todo match frontend..?
+ String fieldName = criteria.getFieldName() + operatorHumanish.replaceAll("_", "");
+ String label = fieldMetaData.getLabel() + " " + operatorHumanish.replaceAll("_", " ");
+ fieldMetaData.setName(fieldName);
+ fieldMetaData.setLabel(label);
+
+ ////////////////////////////////////////////////////////////
+ // in this use case, every field is required and editable //
+ ////////////////////////////////////////////////////////////
+ fieldMetaData.setIsRequired(true);
+ fieldMetaData.setIsEditable(true);
+
+ if(defaultValues.containsKey(fieldName))
+ {
+ fieldMetaData.setDefaultValue(defaultValues.get(fieldName));
+ }
+
+ fieldList.add(fieldMetaData);
+ }
+ }
+
+ ///////////////////////////////////
+ // make output object and return //
+ ///////////////////////////////////
+ DynamicFormWidgetData widgetData = new DynamicFormWidgetData();
+ widgetData.setFieldList(fieldList);
+ widgetData.setMergedDynamicFormValuesIntoFieldName("inputValues");
+
+ if(CollectionUtils.nullSafeIsEmpty(fieldList))
+ {
+ widgetData.setNoFieldsMessage("This Report does not use any Variable Values");
+ }
+
+ return new RenderWidgetOutput(widgetData);
+ }
+ catch(Exception e)
+ {
+ LOG.warn("Error rendering scheduled report values dynamic form widget", e, logPair("queryParams", String.valueOf(input.getQueryParams())));
+ throw (new QException("Error rendering scheduled report values dynamic form widget", e));
+ }
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RunScheduledReportExecuteStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RunScheduledReportExecuteStep.java
new file mode 100644
index 00000000..d3ee6854
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RunScheduledReportExecuteStep.java
@@ -0,0 +1,105 @@
+/*
+ * 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.savedreports;
+
+
+import java.util.List;
+import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
+import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallbackFactory;
+import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+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.processes.RunProcessInput;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.savedreports.ScheduledReport;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class RunScheduledReportExecuteStep implements BackendStep
+{
+ private static final QLogger LOG = QLogger.getLogger(RunScheduledReportExecuteStep.class);
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
+ {
+ Integer scheduledReportId = null;
+ try
+ {
+ List records = runBackendStepInput.getRecords();
+ if(!CollectionUtils.nullSafeHasContents(records))
+ {
+ throw (new QUserFacingException("No scheduled report was selected or found."));
+ }
+
+ ScheduledReport scheduledReport = new ScheduledReport(records.get(0));
+ scheduledReportId = scheduledReport.getId();
+
+ /////////////////////////////////////////////
+ // run the process that renders the report //
+ /////////////////////////////////////////////
+ RunProcessAction runProcessAction = new RunProcessAction();
+ RunProcessInput renderProcessInput = new RunProcessInput();
+ renderProcessInput.setProcessName(RenderSavedReportMetaDataProducer.NAME);
+ renderProcessInput.setCallback(QProcessCallbackFactory.forPrimaryKey("id", scheduledReport.getSavedReportId()));
+ renderProcessInput.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP);
+ renderProcessInput.setAsyncJobCallback(runBackendStepInput.getAsyncJobCallback());
+
+ renderProcessInput.addValue(RenderSavedReportMetaDataProducer.FIELD_NAME_REPORT_FORMAT, scheduledReport.getFormat());
+ renderProcessInput.addValue(RenderSavedReportMetaDataProducer.FIELD_NAME_EMAIL_ADDRESS, scheduledReport.getToAddresses());
+ renderProcessInput.addValue(RenderSavedReportMetaDataProducer.FIELD_NAME_EMAIL_SUBJECT, scheduledReport.getSubject());
+
+ if(StringUtils.hasContent(scheduledReport.getInputValues()))
+ {
+ //////////////////////////
+ // todo variable-values //
+ //////////////////////////
+ }
+
+ RunProcessOutput renderProcessOutput = runProcessAction.execute(renderProcessInput);
+ }
+ catch(QUserFacingException ufe)
+ {
+ LOG.info("Error running scheduled report", ufe, logPair("id", scheduledReportId));
+ throw (ufe);
+ }
+ catch(Exception e)
+ {
+ LOG.warn("Error running scheduled report", e, logPair("id", scheduledReportId));
+ throw (new QException("Error running scheduled report", e));
+ }
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RunScheduledReportMetaDataProducer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RunScheduledReportMetaDataProducer.java
new file mode 100644
index 00000000..15717f22
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RunScheduledReportMetaDataProducer.java
@@ -0,0 +1,76 @@
+/*
+ * 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.savedreports;
+
+
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
+import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.WidgetHtmlLine;
+import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.NoCodeWidgetFrontendComponentMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
+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.model.metadata.processes.QRecordListMetaData;
+import com.kingsrook.qqq.backend.core.model.savedreports.ScheduledReport;
+
+
+/*******************************************************************************
+ ** define process for rendering scheduled reports - that is - a thin layer on
+ ** top of rendering a saved report.
+ *******************************************************************************/
+public class RunScheduledReportMetaDataProducer implements MetaDataProducerInterface
+{
+ public static final String NAME = "runScheduledReport";
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public QProcessMetaData produce(QInstance qInstance) throws QException
+ {
+ QProcessMetaData process = new QProcessMetaData()
+ .withName(NAME)
+ .withLabel("Run Scheduled Report")
+ .withTableName(ScheduledReport.TABLE_NAME)
+ .withIcon(new QIcon().withName("print"))
+
+ .addStep(new QBackendStepMetaData()
+ .withName("execute")
+ .withInputData(new QFunctionInputMetaData().withRecordListMetaData(new QRecordListMetaData()
+ .withTableName(ScheduledReport.TABLE_NAME)))
+ .withCode(new QCodeReference(RunScheduledReportExecuteStep.class)))
+
+ .addStep(new QFrontendStepMetaData()
+ .withName("results")
+ .withComponent(new NoCodeWidgetFrontendComponentMetaData()
+ .withOutput(new WidgetHtmlLine().withVelocityTemplate("Success")))); // todo!!!
+
+ return (process);
+ }
+
+}