@kb-labs/workflow-daemon 2.88.0 → 2.93.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -25,7 +25,8 @@ async function createWorkflowWorker(options) {
25
25
  workspaceRoot,
26
26
  concurrency = 5,
27
27
  defaultTimeout = 12e4,
28
- analytics
28
+ analytics,
29
+ debugMode = process.env["WORKFLOW_DEBUG"] === "true"
29
30
  } = options;
30
31
  let isRunning = false;
31
32
  let stopRequested = false;
@@ -33,8 +34,9 @@ async function createWorkflowWorker(options) {
33
34
  const claimedJobs = /* @__PURE__ */ new Set();
34
35
  const executionBackend = platform2.executionBackend;
35
36
  const runner = new SandboxRunner({
37
+ // IExecutionBackend<unknown> (core-contracts) is structurally compatible with
38
+ // ExecutionBackend (plugin-execution) — both expose the same execute/health/stats/shutdown API.
36
39
  backend: executionBackend,
37
- // ExecutionBackend type from plugin-execution
38
40
  cliApi,
39
41
  workspaceRoot,
40
42
  defaultTimeout
@@ -166,6 +168,10 @@ async function createWorkflowWorker(options) {
166
168
  exprCtx.steps[s.spec.id] = {
167
169
  outputs: s.outputs ?? {}
168
170
  };
171
+ } else if (s.status === "failed" && s.spec.uses === "builtin:approval" && s.spec.id && s.outputs) {
172
+ exprCtx.steps[s.spec.id] = {
173
+ outputs: s.outputs
174
+ };
169
175
  }
170
176
  }
171
177
  }
@@ -185,7 +191,29 @@ async function createWorkflowWorker(options) {
185
191
  continue;
186
192
  }
187
193
  }
194
+ if (debugMode) {
195
+ jobLogger.info("[debug] Expression context for step", {
196
+ runId: run.id,
197
+ jobId: job.id,
198
+ stepId: step.id,
199
+ stepUses: step.spec.uses,
200
+ exprCtx: {
201
+ inputs: exprCtx.inputs,
202
+ steps: exprCtx.steps,
203
+ env: exprCtx.env
204
+ }
205
+ });
206
+ }
188
207
  const interpolatedWith = step.spec.with ? interpolateObject(step.spec.with, exprCtx) : void 0;
208
+ if (debugMode && step.spec.with) {
209
+ jobLogger.info("[debug] Step input interpolation", {
210
+ runId: run.id,
211
+ jobId: job.id,
212
+ stepId: step.id,
213
+ rawWith: step.spec.with,
214
+ resolvedWith: interpolatedWith
215
+ });
216
+ }
189
217
  const stepExecutionId = `wf-${run.id}-${job.id}-${step.id}-${Date.now()}`;
190
218
  const stepLogger = jobLogger.child({
191
219
  operation: "workflow.step",
@@ -195,20 +223,31 @@ async function createWorkflowWorker(options) {
195
223
  spanId: stepExecutionId,
196
224
  invocationId: stepExecutionId
197
225
  });
226
+ if (interpolatedWith) {
227
+ const stateStore = engine.getStateStore();
228
+ await stateStore.updateStep(run.id, job.id, step.id, (draft) => {
229
+ draft.resolvedInputs = interpolatedWith;
230
+ });
231
+ }
198
232
  stepLogger.info("Executing step", {
199
233
  runId: run.id,
200
234
  jobId: job.id,
201
235
  stepId: step.id,
202
- uses: step.spec.uses
236
+ uses: step.spec.uses,
237
+ // Log resolved inputs so failures are debuggable without a debug flag.
238
+ // Sensitive values (tokens, secrets) will appear here — acceptable for
239
+ // server-side structured logs; do not surface in public UI.
240
+ inputs: interpolatedWith
203
241
  });
204
242
  if (step.spec.uses === "builtin:approval") {
243
+ if (step.status === "failed") {
244
+ stepLogger.info("Approval already rejected \u2014 skipping to gate", {
245
+ runId: run.id,
246
+ stepId: step.id
247
+ });
248
+ continue;
249
+ }
205
250
  if (step.status !== "waiting_approval") {
206
- if (interpolatedWith) {
207
- const stateStore = engine.getStateStore();
208
- await stateStore.updateStep(run.id, job.id, step.id, (draft) => {
209
- draft.spec = { ...draft.spec, with: interpolatedWith };
210
- });
211
- }
212
251
  await engine.markStepWaitingApproval(run.id, job.id, step.id);
213
252
  }
214
253
  stepLogger.info("Waiting for approval", {
@@ -227,9 +266,8 @@ async function createWorkflowWorker(options) {
227
266
  break;
228
267
  }
229
268
  if (currentStep.status === "failed") {
230
- const rejectMsg = currentStep.error?.message ?? "Approval rejected";
231
269
  stepLogger.info("Approval rejected", { runId: run.id, stepId: step.id });
232
- throw new Error(rejectMsg);
270
+ break;
233
271
  }
234
272
  }
235
273
  if (stopRequested) {
@@ -388,12 +426,21 @@ async function createWorkflowWorker(options) {
388
426
  });
389
427
  throw error;
390
428
  }
391
- await engine.markStepCompleted(run.id, job.id, step.id, result.status === "success" ? result.outputs : void 0);
429
+ const stepOutputs = result.status === "success" ? result.outputs : void 0;
430
+ await engine.markStepCompleted(run.id, job.id, step.id, stepOutputs);
392
431
  stepLogger.info("Step completed", {
393
432
  runId: run.id,
394
433
  jobId: job.id,
395
434
  stepId: step.id
396
435
  });
436
+ if (debugMode && stepOutputs) {
437
+ stepLogger.info("[debug] Step outputs", {
438
+ runId: run.id,
439
+ jobId: job.id,
440
+ stepId: step.id,
441
+ outputs: stepOutputs
442
+ });
443
+ }
397
444
  }
398
445
  await engine.markJobCompleted(run.id, job.id);
399
446
  const jobDuration = Date.now() - jobStartTime;
@@ -631,12 +678,25 @@ var JobBroker = class {
631
678
  await this.engine.cancelRun(runId);
632
679
  this.logger.info("Job cancelled", { runId });
633
680
  }
681
+ /**
682
+ * Query logs for a specific run, with optional step-level filtering.
683
+ * Used by GET /api/v1/runs/:runId/logs?stepId=...
684
+ */
685
+ async getRunLogs(runId, options) {
686
+ return this._queryLogs(runId, options);
687
+ }
634
688
  /**
635
689
  * Get job logs by run ID.
636
690
  * Returns execution logs with optional filtering by level and pagination.
637
691
  * Uses platform.logs service to query logs by runId metadata.
638
692
  */
639
693
  async getJobLogs(runId, options) {
694
+ return this._queryLogs(runId, options);
695
+ }
696
+ /**
697
+ * Internal: query + filter logs for a given run (optionally narrow to one step).
698
+ */
699
+ async _queryLogs(runId, options) {
640
700
  const run = await this.engine.getRun(runId);
641
701
  if (!run) {
642
702
  return [];
@@ -652,18 +712,22 @@ var JobBroker = class {
652
712
  level: options?.level && options.level !== "all" ? options.level : void 0
653
713
  },
654
714
  {
655
- limit: 1e3,
656
- // Query more logs, then filter in-memory
715
+ limit: 2e3,
657
716
  offset: 0
658
717
  }
659
718
  );
660
- const filteredLogs = queryResult.logs.filter((log) => {
661
- return log.fields.runId === runId || log.fields.executionId === runId || // Also check jobId and stepId for drill-down capability
662
- log.fields.jobId?.toString().startsWith(runId);
719
+ const filtered = queryResult.logs.filter((log) => {
720
+ if (log.fields["runId"] !== runId) {
721
+ return false;
722
+ }
723
+ if (options?.stepId && log.fields["stepId"] !== options.stepId) {
724
+ return false;
725
+ }
726
+ return true;
663
727
  });
664
- const sortedLogs = filteredLogs.sort((a, b) => b.timestamp - a.timestamp);
665
- const paginatedLogs = sortedLogs.slice(offset, offset + limit);
666
- return paginatedLogs.map((log) => ({
728
+ filtered.sort((a, b) => a.timestamp - b.timestamp);
729
+ const page = filtered.slice(offset, offset + limit);
730
+ return page.map((log) => ({
667
731
  timestamp: new Date(log.timestamp).toISOString(),
668
732
  level: log.level,
669
733
  message: log.message,
@@ -1284,6 +1348,10 @@ var WorkflowHostService = class {
1284
1348
  const logs = await this.options.jobBroker.getJobLogs(jobId, options);
1285
1349
  return logs;
1286
1350
  }
1351
+ async getRunLogs(runId, options) {
1352
+ const logs = await this.options.jobBroker.getRunLogs(runId, options);
1353
+ return logs;
1354
+ }
1287
1355
  async cancelJob(tenantId, jobId) {
1288
1356
  await this.options.engine.cancelRun(jobId);
1289
1357
  this.options.logger.info("Job cancelled", { jobId, tenantId });
@@ -1360,7 +1428,7 @@ var WorkflowHostService = class {
1360
1428
  };
1361
1429
  const triggerType = request.trigger?.type === "cron" ? "schedule" : request.trigger?.type === "api" ? "webhook" : "manual";
1362
1430
  const specInputDefs = specInput["inputs"] ?? {};
1363
- const userInputs = request.input && typeof request.input === "object" ? request.input : {};
1431
+ const userInputs = request.inputs ?? (request.input && typeof request.input === "object" ? request.input : {});
1364
1432
  const resolvedInputs = {};
1365
1433
  for (const [key, def] of Object.entries(specInputDefs)) {
1366
1434
  resolvedInputs[key] = key in userInputs ? userInputs[key] : def.default;
@@ -1706,113 +1774,131 @@ function registerJobsAPI(options) {
1706
1774
  // src/api/cron-api.ts
1707
1775
  function registerCronAPI(options) {
1708
1776
  const { server, hostService, logger, observability } = options;
1709
- const registerCronHandler = async (request, reply) => {
1710
- const tenantId = request.headers["x-tenant-id"] ?? "default";
1711
- try {
1712
- const data = await observability.observeOperation(
1713
- "workflow.cron.register",
1714
- () => Promise.resolve(hostService.registerCron(tenantId, request.body))
1715
- );
1716
- return ok(data);
1717
- } catch (error) {
1718
- const message = error instanceof Error ? error.message : "Failed to register cron job";
1719
- if (message === "Cron scheduler not available") {
1720
- return fail(reply, 503, message);
1721
- }
1722
- if (message.startsWith("Missing required fields")) {
1723
- return fail(reply, 400, message);
1777
+ server.post(
1778
+ "/api/v1/crons",
1779
+ { schema: { tags: ["Cron"], summary: "Register a cron job" } },
1780
+ async (request, reply) => {
1781
+ const tenantId = request.headers["x-tenant-id"] ?? "default";
1782
+ try {
1783
+ const data = await observability.observeOperation(
1784
+ "workflow.cron.register",
1785
+ () => Promise.resolve(hostService.registerCron(tenantId, request.body))
1786
+ );
1787
+ return ok(data);
1788
+ } catch (error) {
1789
+ const message = error instanceof Error ? error.message : "Failed to register cron job";
1790
+ if (message === "Cron scheduler not available") {
1791
+ return fail(reply, 503, message);
1792
+ }
1793
+ if (message.startsWith("Missing required fields")) {
1794
+ return fail(reply, 400, message);
1795
+ }
1796
+ logger.error("Failed to register cron job", error instanceof Error ? error : void 0);
1797
+ return fail(reply, 500, message);
1724
1798
  }
1725
- logger.error("Failed to register cron job", error instanceof Error ? error : void 0);
1726
- return fail(reply, 500, message);
1727
1799
  }
1728
- };
1729
- const listCronHandler = async (_request, reply) => {
1730
- try {
1731
- return ok(await observability.observeOperation("workflow.cron.list", () => Promise.resolve(hostService.listCron())));
1732
- } catch (error) {
1733
- const message = error instanceof Error ? error.message : "Failed to list cron jobs";
1734
- if (message === "Cron scheduler not available") {
1735
- return fail(reply, 503, message);
1800
+ );
1801
+ server.get(
1802
+ "/api/v1/crons",
1803
+ { schema: { tags: ["Cron"], summary: "List cron jobs" } },
1804
+ async (_request, reply) => {
1805
+ try {
1806
+ return ok(await observability.observeOperation("workflow.cron.list", () => Promise.resolve(hostService.listCron())));
1807
+ } catch (error) {
1808
+ const message = error instanceof Error ? error.message : "Failed to list cron jobs";
1809
+ if (message === "Cron scheduler not available") {
1810
+ return fail(reply, 503, message);
1811
+ }
1812
+ logger.error("Failed to list cron jobs", error instanceof Error ? error : void 0);
1813
+ return fail(reply, 500, message);
1736
1814
  }
1737
- logger.error("Failed to list cron jobs", error instanceof Error ? error : void 0);
1738
- return fail(reply, 500, message);
1739
1815
  }
1740
- };
1741
- const unregisterCronHandler = async (request, reply) => {
1742
- const { id } = request.params;
1743
- const tenantId = request.headers["x-tenant-id"] ?? "default";
1744
- try {
1745
- const data = await observability.observeOperation(
1746
- "workflow.cron.unregister",
1747
- () => Promise.resolve(hostService.unregisterCron(tenantId, id))
1748
- );
1749
- return ok(data);
1750
- } catch (error) {
1751
- const message = error instanceof Error ? error.message : "Failed to unregister cron job";
1752
- if (message === "Cron scheduler not available") {
1753
- return fail(reply, 503, message);
1816
+ );
1817
+ server.delete(
1818
+ "/api/v1/crons/:id",
1819
+ { schema: { tags: ["Cron"], summary: "Unregister a cron job" } },
1820
+ async (request, reply) => {
1821
+ const { id } = request.params;
1822
+ const tenantId = request.headers["x-tenant-id"] ?? "default";
1823
+ try {
1824
+ const data = await observability.observeOperation(
1825
+ "workflow.cron.unregister",
1826
+ () => Promise.resolve(hostService.unregisterCron(tenantId, id))
1827
+ );
1828
+ return ok(data);
1829
+ } catch (error) {
1830
+ const message = error instanceof Error ? error.message : "Failed to unregister cron job";
1831
+ if (message === "Cron scheduler not available") {
1832
+ return fail(reply, 503, message);
1833
+ }
1834
+ logger.error("Failed to unregister cron job", error instanceof Error ? error : void 0);
1835
+ return fail(reply, 500, message);
1754
1836
  }
1755
- logger.error("Failed to unregister cron job", error instanceof Error ? error : void 0);
1756
- return fail(reply, 500, message);
1757
1837
  }
1758
- };
1759
- const triggerCronHandler = async (request, reply) => {
1760
- const { id } = request.params;
1761
- const tenantId = request.headers["x-tenant-id"] ?? "default";
1762
- try {
1763
- const data = await observability.observeOperation("workflow.cron.trigger", () => hostService.triggerCron(tenantId, id));
1764
- return ok(data);
1765
- } catch (error) {
1766
- const message = error instanceof Error ? error.message : "Failed to trigger cron job";
1767
- if (message === "Cron scheduler not available") {
1768
- return fail(reply, 503, message);
1838
+ );
1839
+ server.post(
1840
+ "/api/v1/crons/:id/trigger",
1841
+ { schema: { tags: ["Cron"], summary: "Trigger a cron job immediately" } },
1842
+ async (request, reply) => {
1843
+ const { id } = request.params;
1844
+ const tenantId = request.headers["x-tenant-id"] ?? "default";
1845
+ try {
1846
+ const data = await observability.observeOperation("workflow.cron.trigger", () => hostService.triggerCron(tenantId, id));
1847
+ return ok(data);
1848
+ } catch (error) {
1849
+ const message = error instanceof Error ? error.message : "Failed to trigger cron job";
1850
+ if (message === "Cron scheduler not available") {
1851
+ return fail(reply, 503, message);
1852
+ }
1853
+ logger.error("Failed to trigger cron job", error instanceof Error ? error : void 0);
1854
+ return fail(reply, 500, message);
1769
1855
  }
1770
- logger.error("Failed to trigger cron job", error instanceof Error ? error : void 0);
1771
- return fail(reply, 500, message);
1772
1856
  }
1773
- };
1774
- const pauseCronHandler = async (request, reply) => {
1775
- const { id } = request.params;
1776
- const tenantId = request.headers["x-tenant-id"] ?? "default";
1777
- try {
1778
- const data = await observability.observeOperation(
1779
- "workflow.cron.pause",
1780
- () => Promise.resolve(hostService.pauseCron(tenantId, id))
1781
- );
1782
- return ok(data);
1783
- } catch (error) {
1784
- const message = error instanceof Error ? error.message : "Failed to pause cron job";
1785
- if (message === "Cron scheduler not available") {
1786
- return fail(reply, 503, message);
1857
+ );
1858
+ server.post(
1859
+ "/api/v1/crons/:id/pause",
1860
+ { schema: { tags: ["Cron"], summary: "Pause a cron job" } },
1861
+ async (request, reply) => {
1862
+ const { id } = request.params;
1863
+ const tenantId = request.headers["x-tenant-id"] ?? "default";
1864
+ try {
1865
+ const data = await observability.observeOperation(
1866
+ "workflow.cron.pause",
1867
+ () => Promise.resolve(hostService.pauseCron(tenantId, id))
1868
+ );
1869
+ return ok(data);
1870
+ } catch (error) {
1871
+ const message = error instanceof Error ? error.message : "Failed to pause cron job";
1872
+ if (message === "Cron scheduler not available") {
1873
+ return fail(reply, 503, message);
1874
+ }
1875
+ logger.error("Failed to pause cron job", error instanceof Error ? error : void 0);
1876
+ return fail(reply, 500, message);
1787
1877
  }
1788
- logger.error("Failed to pause cron job", error instanceof Error ? error : void 0);
1789
- return fail(reply, 500, message);
1790
1878
  }
1791
- };
1792
- const resumeCronHandler = async (request, reply) => {
1793
- const { id } = request.params;
1794
- const tenantId = request.headers["x-tenant-id"] ?? "default";
1795
- try {
1796
- const data = await observability.observeOperation(
1797
- "workflow.cron.resume",
1798
- () => Promise.resolve(hostService.resumeCron(tenantId, id))
1799
- );
1800
- return ok(data);
1801
- } catch (error) {
1802
- const message = error instanceof Error ? error.message : "Failed to resume cron job";
1803
- if (message === "Cron scheduler not available") {
1804
- return fail(reply, 503, message);
1879
+ );
1880
+ server.post(
1881
+ "/api/v1/crons/:id/resume",
1882
+ { schema: { tags: ["Cron"], summary: "Resume a cron job" } },
1883
+ async (request, reply) => {
1884
+ const { id } = request.params;
1885
+ const tenantId = request.headers["x-tenant-id"] ?? "default";
1886
+ try {
1887
+ const data = await observability.observeOperation(
1888
+ "workflow.cron.resume",
1889
+ () => Promise.resolve(hostService.resumeCron(tenantId, id))
1890
+ );
1891
+ return ok(data);
1892
+ } catch (error) {
1893
+ const message = error instanceof Error ? error.message : "Failed to resume cron job";
1894
+ if (message === "Cron scheduler not available") {
1895
+ return fail(reply, 503, message);
1896
+ }
1897
+ logger.error("Failed to resume cron job", error instanceof Error ? error : void 0);
1898
+ return fail(reply, 500, message);
1805
1899
  }
1806
- logger.error("Failed to resume cron job", error instanceof Error ? error : void 0);
1807
- return fail(reply, 500, message);
1808
1900
  }
1809
- };
1810
- server.post("/api/v1/crons", { schema: { tags: ["Cron"], summary: "Register a cron job" } }, registerCronHandler);
1811
- server.get("/api/v1/crons", { schema: { tags: ["Cron"], summary: "List cron jobs" } }, listCronHandler);
1812
- server.delete("/api/v1/crons/:id", { schema: { tags: ["Cron"], summary: "Unregister a cron job" } }, unregisterCronHandler);
1813
- server.post("/api/v1/crons/:id/trigger", { schema: { tags: ["Cron"], summary: "Trigger a cron job immediately" } }, triggerCronHandler);
1814
- server.post("/api/v1/crons/:id/pause", { schema: { tags: ["Cron"], summary: "Pause a cron job" } }, pauseCronHandler);
1815
- server.post("/api/v1/crons/:id/resume", { schema: { tags: ["Cron"], summary: "Resume a cron job" } }, resumeCronHandler);
1901
+ );
1816
1902
  }
1817
1903
 
1818
1904
  // src/api/workflows-api.ts
@@ -1939,6 +2025,33 @@ function registerWorkflowsAPI(options) {
1939
2025
  return fail(reply, 500, error instanceof Error ? error.message : "Failed to get run");
1940
2026
  }
1941
2027
  });
2028
+ server.get(
2029
+ "/api/v1/runs/:runId/logs",
2030
+ { schema: { tags: ["Runs"], summary: "Get execution logs for a run" } },
2031
+ async (request, reply) => {
2032
+ const { runId } = request.params;
2033
+ const { stepId, level, limit, offset } = request.query;
2034
+ try {
2035
+ const logs = await observability.observeOperation(
2036
+ "workflow.run.logs",
2037
+ () => hostService.getRunLogs(runId, {
2038
+ stepId,
2039
+ level,
2040
+ limit: limit ? parseInt(limit, 10) : void 0,
2041
+ offset: offset ? parseInt(offset, 10) : void 0
2042
+ })
2043
+ );
2044
+ return ok({ logs, runId, stepId });
2045
+ } catch (error) {
2046
+ const message = error instanceof Error ? error.message : "Failed to get run logs";
2047
+ if (message === "Run not found") {
2048
+ return fail(reply, 404, message);
2049
+ }
2050
+ logger.error("[workflows-api] Error getting run logs", error instanceof Error ? error : void 0);
2051
+ return fail(reply, 500, message);
2052
+ }
2053
+ }
2054
+ );
1942
2055
  server.get("/api/v1/runs/:runId/events", { schema: { hide: true } }, async (request, reply) => {
1943
2056
  const { runId } = request.params;
1944
2057
  const run = await observability.observeOperation("workflow.run.events", () => engine.getRun(runId));
@@ -2281,7 +2394,7 @@ async function createServer(options) {
2281
2394
  });
2282
2395
  server.get("/ready", async () => {
2283
2396
  const metrics = await hostService.getMetrics();
2284
- const checks = buildWorkflowChecks({ workflowService, cronScheduler, metrics });
2397
+ const checks = buildWorkflowReadinessChecks({ workflowService, cronScheduler, metrics });
2285
2398
  const hasErrors = checks.some((entry) => entry.status === "error");
2286
2399
  const hasWarnings = checks.some((entry) => entry.status === "warn");
2287
2400
  return createServiceReadyResponse({
@@ -2333,6 +2446,16 @@ function resolveWorkflowHealthStatus(metrics) {
2333
2446
  return "healthy";
2334
2447
  }
2335
2448
  function buildWorkflowChecks(input) {
2449
+ return [
2450
+ ...buildWorkflowReadinessChecks(input),
2451
+ {
2452
+ id: "workflow-failures",
2453
+ status: input.metrics.runs.failed > 0 || input.metrics.jobs.failed > 0 ? "warn" : "ok",
2454
+ message: input.metrics.runs.failed > 0 || input.metrics.jobs.failed > 0 ? `${input.metrics.runs.failed} failed runs, ${input.metrics.jobs.failed} failed jobs retained in history` : "No failed workflow runs or jobs in retained history"
2455
+ }
2456
+ ];
2457
+ }
2458
+ function buildWorkflowReadinessChecks(input) {
2336
2459
  return [
2337
2460
  {
2338
2461
  id: "workflow-engine",
@@ -2348,11 +2471,6 @@ function buildWorkflowChecks(input) {
2348
2471
  id: "cron-scheduler",
2349
2472
  status: input.cronScheduler ? "ok" : "warn",
2350
2473
  message: input.cronScheduler ? "Cron scheduler available" : "Cron scheduler not configured"
2351
- },
2352
- {
2353
- id: "workflow-failures",
2354
- status: input.metrics.runs.failed > 0 || input.metrics.jobs.failed > 0 ? "warn" : "ok",
2355
- message: input.metrics.runs.failed > 0 || input.metrics.jobs.failed > 0 ? `${input.metrics.runs.failed} failed runs, ${input.metrics.jobs.failed} failed jobs retained in history` : "No failed workflow runs or jobs in retained history"
2356
2474
  }
2357
2475
  ];
2358
2476
  }
@@ -2429,7 +2547,13 @@ async function bootstrap(cwd = process.cwd()) {
2429
2547
  executionId: startupSpanId
2430
2548
  }
2431
2549
  });
2432
- bootstrapLogger.info("Workflow daemon starting", { repoRoot, projectRoot });
2550
+ const debugMode = process.env["WORKFLOW_DEBUG"] === "true";
2551
+ bootstrapLogger.info("Workflow daemon starting", { repoRoot, projectRoot, debugMode });
2552
+ if (debugMode) {
2553
+ bootstrapLogger.warn(
2554
+ "[WORKFLOW_DEBUG=true] Verbose debug logging is ON \u2014 step inputs, outputs, and expr contexts will be logged. Disable in production (unset WORKFLOW_DEBUG or set to false)."
2555
+ );
2556
+ }
2433
2557
  const createWorkflowLogger = (service, operation, bindings) => createCorrelatedLogger(platform.logger, {
2434
2558
  serviceId: "workflow",
2435
2559
  logsSource: "workflow",
@@ -2499,7 +2623,7 @@ async function bootstrap(cwd = process.cwd()) {
2499
2623
  logger: createWorkflowLogger("api", "workflow.api")
2500
2624
  });
2501
2625
  const port = parseInt(process.env.WORKFLOW_PORT || "7778", 10);
2502
- await server.listen({ port, host: "0.0.0.0" });
2626
+ await server.listen({ port, host: process.env.WORKFLOW_HOST ?? "0.0.0.0" });
2503
2627
  bootstrapLogger.info("HTTP API listening", { port });
2504
2628
  serverInstance = server;
2505
2629
  bootstrapLogger.info("Creating WorkflowWorker");
@@ -2510,7 +2634,8 @@ async function bootstrap(cwd = process.cwd()) {
2510
2634
  analytics: platform.analytics,
2511
2635
  platform,
2512
2636
  workspaceRoot: projectRoot,
2513
- concurrency: parseInt(process.env.WORKFLOW_CONCURRENCY || "5", 10)
2637
+ concurrency: parseInt(process.env.WORKFLOW_CONCURRENCY || "5", 10),
2638
+ debugMode
2514
2639
  });
2515
2640
  workerInstance = worker;
2516
2641
  bootstrapLogger.info("Starting WorkflowWorker");