No task artifacts yet.
+
+
Stdout
+ {(selectedTask.stdoutLines ?? []).length > 0 ? (
+
+ {(selectedTask.stdoutLines ?? []).map((line: string, index: number) => (
+ - {line}
+ ))}
+
+ ) : (
+
No stdout lines.
+ )}
+
+
+
Stderr
+ {(selectedTask.stderrLines ?? []).length > 0 ? (
+
+ {(selectedTask.stderrLines ?? []).map((line: string, index: number) => (
+ - {line}
+ ))}
+
+ ) : (
+
No stderr lines.
+ )}
+
Execution Log
{(selectedTask.logLines ?? []).length > 0 ? (
diff --git a/apps/worker/src/contracts/execution-context.ts b/apps/worker/src/contracts/execution-context.ts
index c0bb582..ca80862 100644
--- a/apps/worker/src/contracts/execution-context.ts
+++ b/apps/worker/src/contracts/execution-context.ts
@@ -1,14 +1,39 @@
export type ExecutorType = "python" | "docker" | "http";
export type TaskStatus = "pending" | "queued" | "running" | "success" | "failed";
+export type TaskStatusCounts = {
+ pending: number;
+ queued: number;
+ running: number;
+ success: number;
+ failed: number;
+};
export type TaskExecutionSummary = {
outcome: "success" | "failed";
executorType: ExecutorType;
assetCount: number;
artifactIds: string[];
+ stdoutLineCount: number;
+ stderrLineCount: number;
errorMessage?: string;
};
+export type RunExecutionSummary = {
+ totalTaskCount: number;
+ completedTaskCount: number;
+ artifactCount: number;
+ stdoutLineCount: number;
+ stderrLineCount: number;
+ failedTaskIds: string[];
+ taskCounts: TaskStatusCounts;
+};
+
+export type ExecutorExecutionResult = {
+ result: unknown;
+ stdoutLines?: string[];
+ stderrLines?: string[];
+};
+
export type TaskRecord = {
id: string;
workflowRunId?: string;
@@ -26,6 +51,8 @@ export type TaskRecord = {
finishedAt?: string;
durationMs?: number;
logLines?: string[];
+ stdoutLines?: string[];
+ stderrLines?: string[];
summary?: TaskExecutionSummary;
lastResultPreview?: Record;
};
diff --git a/apps/worker/src/executors/docker-executor.ts b/apps/worker/src/executors/docker-executor.ts
index f8b4c74..c4600c7 100644
--- a/apps/worker/src/executors/docker-executor.ts
+++ b/apps/worker/src/executors/docker-executor.ts
@@ -1,10 +1,18 @@
-import type { ExecutionContext, TaskRecord } from "../contracts/execution-context.ts";
+import type {
+ ExecutionContext,
+ ExecutorExecutionResult,
+ TaskRecord,
+} from "../contracts/execution-context.ts";
export class DockerExecutor {
executionCount = 0;
- async execute(task: TaskRecord, _context: ExecutionContext) {
+ async execute(task: TaskRecord, _context: ExecutionContext): Promise {
this.executionCount += 1;
- return { taskId: task.id, executor: "docker" as const };
+ return {
+ result: { taskId: task.id, executor: "docker" as const },
+ stdoutLines: [`docker executor processed ${task.nodeId}`],
+ stderrLines: [],
+ };
}
}
diff --git a/apps/worker/src/executors/http-executor.ts b/apps/worker/src/executors/http-executor.ts
index 608520a..509c829 100644
--- a/apps/worker/src/executors/http-executor.ts
+++ b/apps/worker/src/executors/http-executor.ts
@@ -1,10 +1,18 @@
-import type { ExecutionContext, TaskRecord } from "../contracts/execution-context.ts";
+import type {
+ ExecutionContext,
+ ExecutorExecutionResult,
+ TaskRecord,
+} from "../contracts/execution-context.ts";
export class HttpExecutor {
executionCount = 0;
- async execute(task: TaskRecord, _context: ExecutionContext) {
+ async execute(task: TaskRecord, _context: ExecutionContext): Promise {
this.executionCount += 1;
- return { taskId: task.id, executor: "http" as const };
+ return {
+ result: { taskId: task.id, executor: "http" as const },
+ stdoutLines: [`http executor processed ${task.nodeId}`],
+ stderrLines: [],
+ };
}
}
diff --git a/apps/worker/src/executors/python-executor.ts b/apps/worker/src/executors/python-executor.ts
index d19cf89..66e512b 100644
--- a/apps/worker/src/executors/python-executor.ts
+++ b/apps/worker/src/executors/python-executor.ts
@@ -1,10 +1,18 @@
-import type { ExecutionContext, TaskRecord } from "../contracts/execution-context.ts";
+import type {
+ ExecutionContext,
+ ExecutorExecutionResult,
+ TaskRecord,
+} from "../contracts/execution-context.ts";
export class PythonExecutor {
executionCount = 0;
- async execute(task: TaskRecord, _context: ExecutionContext) {
+ async execute(task: TaskRecord, _context: ExecutionContext): Promise {
this.executionCount += 1;
- return { taskId: task.id, executor: "python" as const };
+ return {
+ result: { taskId: task.id, executor: "python" as const },
+ stdoutLines: [`python executor processed ${task.nodeId}`],
+ stderrLines: [],
+ };
}
}
diff --git a/apps/worker/src/runtime/mongo-worker-store.ts b/apps/worker/src/runtime/mongo-worker-store.ts
index 8bdf921..3b8ee21 100644
--- a/apps/worker/src/runtime/mongo-worker-store.ts
+++ b/apps/worker/src/runtime/mongo-worker-store.ts
@@ -4,8 +4,10 @@ import type { Db } from "mongodb";
import type {
ExecutorType,
+ RunExecutionSummary,
TaskExecutionSummary,
TaskRecord,
+ TaskStatusCounts,
TaskStatus,
} from "../contracts/execution-context.ts";
@@ -16,6 +18,10 @@ type WorkflowRunDocument = {
status: "queued" | "running" | "success" | "failed";
triggeredBy: string;
assetIds: string[];
+ startedAt?: string;
+ finishedAt?: string;
+ durationMs?: number;
+ summary?: RunExecutionSummary;
createdAt: string;
updatedAt: string;
};
@@ -47,6 +53,8 @@ type RunTaskDocument = {
finishedAt?: string;
durationMs?: number;
logLines?: string[];
+ stdoutLines?: string[];
+ stderrLines?: string[];
summary?: TaskExecutionSummary;
lastResultPreview?: Record;
createdAt: string;
@@ -75,11 +83,58 @@ function toTaskRecord(task: RunTaskDocument): TaskRecord {
finishedAt: task.finishedAt,
durationMs: task.durationMs,
logLines: task.logLines ?? [],
+ stdoutLines: task.stdoutLines ?? [],
+ stderrLines: task.stderrLines ?? [],
summary: task.summary,
lastResultPreview: task.lastResultPreview,
};
}
+function buildTaskStatusCounts(tasks: TaskRecord[]): TaskStatusCounts {
+ const counts: TaskStatusCounts = {
+ pending: 0,
+ queued: 0,
+ running: 0,
+ success: 0,
+ failed: 0,
+ };
+
+ for (const task of tasks) {
+ counts[task.status] += 1;
+ }
+
+ return counts;
+}
+
+function buildRunExecutionSummary(tasks: TaskRecord[]): RunExecutionSummary {
+ const taskCounts = buildTaskStatusCounts(tasks);
+ return {
+ totalTaskCount: tasks.length,
+ completedTaskCount: taskCounts.success + taskCounts.failed,
+ artifactCount: tasks.reduce((total, task) => total + (task.outputArtifactIds?.length ?? 0), 0),
+ stdoutLineCount: tasks.reduce((total, task) => total + (task.stdoutLines?.length ?? 0), 0),
+ stderrLineCount: tasks.reduce((total, task) => total + (task.stderrLines?.length ?? 0), 0),
+ failedTaskIds: tasks.filter((task) => task.status === "failed").map((task) => task.id),
+ taskCounts,
+ };
+}
+
+function minIso(values: Array) {
+ const filtered = values.filter((value): value is string => Boolean(value));
+ if (filtered.length === 0) {
+ return undefined;
+ }
+ return filtered.reduce((current, value) => (value < current ? value : current));
+}
+
+function maxIso(values: Array) {
+ const filtered = values.filter((value): value is string => Boolean(value));
+ if (filtered.length === 0) {
+ return undefined;
+ }
+ return filtered.reduce((current, value) => (value > current ? value : current));
+}
+
export class MongoWorkerStore {
private readonly db: Db;
@@ -173,7 +228,9 @@ export class MongoWorkerStore {
finishedAt: string;
durationMs: number;
summary: TaskExecutionSummary;
- logLine: string;
+ stdoutLines: string[];
+ stderrLines: string[];
+ logLines: string[];
lastResultPreview?: Record;
},
) {
@@ -184,13 +241,15 @@ export class MongoWorkerStore {
status: "success",
finishedAt: input.finishedAt,
durationMs: input.durationMs,
+ stdoutLines: input.stdoutLines,
+ stderrLines: input.stderrLines,
summary: input.summary,
lastResultPreview: input.lastResultPreview,
updatedAt: input.finishedAt,
},
$push: {
logLines: {
- $each: [input.logLine],
+ $each: input.logLines,
},
},
},
@@ -204,7 +263,9 @@ export class MongoWorkerStore {
finishedAt: string;
durationMs: number;
summary: TaskExecutionSummary;
- logLine: string;
+ stdoutLines: string[];
+ stderrLines: string[];
+ logLines: string[];
},
) {
await this.db.collection("run_tasks").updateOne(
@@ -215,12 +276,14 @@ export class MongoWorkerStore {
errorMessage,
finishedAt: input.finishedAt,
durationMs: input.durationMs,
+ stdoutLines: input.stdoutLines,
+ stderrLines: input.stderrLines,
summary: input.summary,
updatedAt: input.finishedAt,
},
$push: {
logLines: {
- $each: [input.logLine],
+ $each: input.logLines,
},
},
},
@@ -273,14 +336,28 @@ export class MongoWorkerStore {
status = "running";
}
+ const startedAt = minIso(tasks.map((task) => task.startedAt));
+ const finishedAt =
+ status === "success" || status === "failed"
+ ? maxIso(tasks.map((task) => task.finishedAt))
+ : undefined;
+ const summary = buildRunExecutionSummary(tasks);
+ const updateSet: Partial = {
+ status,
+ summary,
+ updatedAt: nowIso(),
+ };
+ if (startedAt) {
+ updateSet.startedAt = startedAt;
+ }
+ if (finishedAt) {
+ updateSet.finishedAt = finishedAt;
+ updateSet.durationMs = Math.max(Date.parse(finishedAt) - Date.parse(startedAt ?? finishedAt), 0);
+ }
+
await this.db.collection("workflow_runs").updateOne(
{ _id: runId },
- {
- $set: {
- status,
- updatedAt: nowIso(),
- },
- },
+ { $set: updateSet },
);
}
}
diff --git a/apps/worker/src/runtime/worker-runtime.ts b/apps/worker/src/runtime/worker-runtime.ts
index 62e7c46..5ab3ecd 100644
--- a/apps/worker/src/runtime/worker-runtime.ts
+++ b/apps/worker/src/runtime/worker-runtime.ts
@@ -3,6 +3,7 @@ import { HttpExecutor } from "../executors/http-executor.ts";
import { PythonExecutor } from "../executors/python-executor.ts";
import type {
ExecutionContext,
+ ExecutorExecutionResult,
ExecutorType,
TaskExecutionSummary,
TaskRecord,
@@ -47,13 +48,15 @@ export class WorkerRuntime {
};
try {
- const result = await this.executors[task.executorType as ExecutorType].execute(task, context);
+ const execution = this.normalizeExecutionResult(
+ await this.executors[task.executorType as ExecutorType].execute(task, context),
+ );
const artifact = await this.store.createTaskArtifact(task, {
nodeId: task.nodeId,
nodeType: task.nodeType,
executorType: task.executorType,
assetIds: task.assetIds,
- result,
+ result: execution.result,
});
const finishedAt = new Date().toISOString();
const summary: TaskExecutionSummary = {
@@ -61,13 +64,21 @@ export class WorkerRuntime {
executorType: task.executorType,
assetCount: task.assetIds?.length ?? 0,
artifactIds: [artifact._id],
+ stdoutLineCount: execution.stdoutLines.length,
+ stderrLineCount: execution.stderrLines.length,
};
await this.store.markTaskSuccess(task.id, {
finishedAt,
durationMs: this.computeDurationMs(startedAt, finishedAt),
summary,
- logLine: "Executor completed successfully",
- lastResultPreview: this.createResultPreview(result),
+ stdoutLines: execution.stdoutLines,
+ stderrLines: execution.stderrLines,
+ logLines: this.createTaskLogLines(
+ execution.stdoutLines,
+ execution.stderrLines,
+ "Executor completed successfully",
+ ),
+ lastResultPreview: this.createResultPreview(execution.result),
});
if (task.workflowRunId) {
await this.store.queueReadyDependents(task.workflowRunId);
@@ -75,20 +86,28 @@ export class WorkerRuntime {
}
return this.store.getRunTask(task.id) ?? { ...task, status: "success" };
} catch (error) {
- const message = error instanceof Error ? error.message : "worker execution failed";
+ const executionError = this.normalizeExecutionError(error);
const finishedAt = new Date().toISOString();
const summary: TaskExecutionSummary = {
outcome: "failed",
executorType: task.executorType,
assetCount: task.assetIds?.length ?? 0,
artifactIds: [],
- errorMessage: message,
+ stdoutLineCount: executionError.stdoutLines.length,
+ stderrLineCount: executionError.stderrLines.length,
+ errorMessage: executionError.message,
};
- await this.store.markTaskFailed(task.id, message, {
+ await this.store.markTaskFailed(task.id, executionError.message, {
finishedAt,
durationMs: this.computeDurationMs(startedAt, finishedAt),
summary,
- logLine: `Execution failed: ${message}`,
+ stdoutLines: executionError.stdoutLines,
+ stderrLines: executionError.stderrLines,
+ logLines: this.createTaskLogLines(
+ executionError.stdoutLines,
+ executionError.stderrLines,
+ `Execution failed: ${executionError.message}`,
+ ),
});
if (task.workflowRunId) {
await this.store.refreshRunStatus(task.workflowRunId);
@@ -111,4 +130,47 @@ export class WorkerRuntime {
}
return undefined;
}
+
+ private normalizeExecutionResult(result: unknown): ExecutorExecutionResult {
+ if (result && typeof result === "object" && "result" in result) {
+ const execution = result as ExecutorExecutionResult;
+ return {
+ result: execution.result,
+ stdoutLines: execution.stdoutLines ?? [],
+ stderrLines: execution.stderrLines ?? [],
+ };
+ }
+
+ return {
+ result,
+ stdoutLines: [],
+ stderrLines: [],
+ };
+ }
+
+ private normalizeExecutionError(error: unknown) {
+ const message = error instanceof Error ? error.message : "worker execution failed";
+ const stdoutLines =
+ error && typeof error === "object" && "stdoutLines" in error && Array.isArray(error.stdoutLines)
+ ? error.stdoutLines.filter((line): line is string => typeof line === "string")
+ : [];
+ const stderrLines =
+ error && typeof error === "object" && "stderrLines" in error && Array.isArray(error.stderrLines)
+ ? error.stderrLines.filter((line): line is string => typeof line === "string")
+ : [];
+
+ return {
+ message,
+ stdoutLines,
+ stderrLines,
+ };
+ }
+
+ private createTaskLogLines(stdoutLines: string[], stderrLines: string[], terminalLine: string) {
+ return [
+ ...stdoutLines.map((line) => `stdout: ${line}`),
+ ...stderrLines.map((line) => `stderr: ${line}`),
+ terminalLine,
+ ];
+ }
}
diff --git a/apps/worker/test/mongo-worker-runtime.spec.ts b/apps/worker/test/mongo-worker-runtime.spec.ts
index 83dfab0..7f8a1e1 100644
--- a/apps/worker/test/mongo-worker-runtime.spec.ts
+++ b/apps/worker/test/mongo-worker-runtime.spec.ts
@@ -189,14 +189,27 @@ test("worker marks the run successful after the final queued task completes", as
assert.ok(typeof task?.startedAt === "string");
assert.ok(typeof task?.finishedAt === "string");
assert.ok(typeof task?.durationMs === "number");
+ assert.deepEqual(task?.stdoutLines, ["python executor processed export-delivery-package"]);
+ assert.deepEqual(task?.stderrLines, []);
assert.equal(task?.summary?.outcome, "success");
assert.equal(task?.summary?.executorType, "python");
+ assert.equal(task?.summary?.stdoutLineCount, 1);
+ assert.equal(task?.summary?.stderrLineCount, 0);
assert.match(task?.logLines?.[0] ?? "", /claimed/i);
+ assert.match(task?.logLines?.[1] ?? "", /stdout:/i);
assert.match(task?.logLines?.at(-1) ?? "", /completed/i);
assert.deepEqual(task?.lastResultPreview, {
taskId: "task-export",
executor: "python",
});
+ assert.ok(typeof run?.startedAt === "string");
+ assert.ok(typeof run?.finishedAt === "string");
+ assert.ok(typeof run?.durationMs === "number");
+ assert.equal(run?.summary?.totalTaskCount, 1);
+ assert.equal(run?.summary?.artifactCount, 1);
+ assert.equal(run?.summary?.stdoutLineCount, 1);
+ assert.equal(run?.summary?.stderrLineCount, 0);
+ assert.equal(run?.summary?.taskCounts?.success, 1);
});
test("worker passes bound asset ids into the execution context and task artifacts", async (t) => {
@@ -209,8 +222,12 @@ test("worker passes bound asset ids into the execution context and task artifact
capturedTask = task;
capturedContext = context;
return {
- taskId: task.id,
- assetIds: context.assetIds,
+ result: {
+ taskId: task.id,
+ assetIds: context.assetIds,
+ },
+ stdoutLines: ["custom executor started"],
+ stderrLines: [],
};
},
},
@@ -262,6 +279,8 @@ test("worker passes bound asset ids into the execution context and task artifact
assert.equal(storedTask?.summary?.outcome, "success");
assert.equal(storedTask?.summary?.assetCount, 1);
assert.deepEqual(storedTask?.summary?.artifactIds, [artifact?._id]);
+ assert.deepEqual(storedTask?.stdoutLines, ["custom executor started"]);
+ assert.deepEqual(storedTask?.stderrLines, []);
});
test("worker persists failure summaries and task log lines when execution throws", async (t) => {
@@ -269,7 +288,10 @@ test("worker persists failure summaries and task log lines when execution throws
executors: {
python: {
async execute() {
- throw new Error("intentional executor failure");
+ throw Object.assign(new Error("intentional executor failure"), {
+ stdoutLines: ["failure path entered"],
+ stderrLines: ["stack trace line"],
+ });
},
},
},
@@ -315,10 +337,21 @@ test("worker persists failure summaries and task log lines when execution throws
assert.equal(task?.errorMessage, "intentional executor failure");
assert.equal(task?.summary?.outcome, "failed");
assert.equal(task?.summary?.executorType, "python");
+ assert.equal(task?.summary?.stdoutLineCount, 1);
+ assert.equal(task?.summary?.stderrLineCount, 1);
assert.equal(task?.summary?.artifactIds?.length ?? 0, 0);
assert.ok(typeof task?.startedAt === "string");
assert.ok(typeof task?.finishedAt === "string");
assert.ok(typeof task?.durationMs === "number");
+ assert.deepEqual(task?.stdoutLines, ["failure path entered"]);
+ assert.deepEqual(task?.stderrLines, ["stack trace line"]);
assert.match(task?.logLines?.[0] ?? "", /claimed/i);
+ assert.match(task?.logLines?.[1] ?? "", /stdout:/i);
+ assert.match(task?.logLines?.[2] ?? "", /stderr:/i);
assert.match(task?.logLines?.at(-1) ?? "", /intentional executor failure/i);
+ assert.equal(run?.summary?.totalTaskCount, 1);
+ assert.equal(run?.summary?.taskCounts?.failed, 1);
+ assert.equal(run?.summary?.stdoutLineCount, 1);
+ assert.equal(run?.summary?.stderrLineCount, 1);
+ assert.deepEqual(run?.summary?.failedTaskIds, ["task-failure"]);
});
diff --git a/design/03-workflows/workflow-execution-model.md b/design/03-workflows/workflow-execution-model.md
index ec982e6..8acfd63 100644
--- a/design/03-workflows/workflow-execution-model.md
+++ b/design/03-workflows/workflow-execution-model.md
@@ -288,11 +288,26 @@ The worker-backed runtime now persists task execution summaries directly on `run
- `startedAt` and `finishedAt`
- `durationMs`
- appended `logLines`
+- captured `stdoutLines` and `stderrLines`
- structured `summary` with outcome, executor, asset count, artifact ids, and failure text when present
- `lastResultPreview` for a lightweight selected-task preview in the Runs workspace
This makes the run detail view stable even when artifacts are large or delayed and keeps task-level observability queryable without reopening every artifact payload.
+The current runtime also aggregates execution state back onto `workflow_runs`. Each refresh computes:
+
+- run-level `startedAt` and `finishedAt`
+- run-level `durationMs`
+- `summary.totalTaskCount`
+- `summary.completedTaskCount`
+- `summary.taskCounts`
+- `summary.artifactCount`
+- `summary.stdoutLineCount`
+- `summary.stderrLineCount`
+- `summary.failedTaskIds`
+
+This allows the Runs workspace to render a stable top-level run summary without client-side recomputation across every task document.
+
The API and worker runtimes now both have direct integration coverage against a real Mongo runtime through `mongodb-memory-server`, in addition to the older in-memory contract tests.
The first web authoring surface already follows the three-pane layout contract with:
diff --git a/design/04-ui-ux/information-architecture-and-key-screens.md b/design/04-ui-ux/information-architecture-and-key-screens.md
index f844adf..6bb514a 100644
--- a/design/04-ui-ux/information-architecture-and-key-screens.md
+++ b/design/04-ui-ux/information-architecture-and-key-screens.md
@@ -187,10 +187,18 @@ V1 run detail should render the selected task as a stable operations panel, not
- task status and executor
- task duration and timestamps
- a concise execution summary
+- separated stdout and stderr sections
- appended log lines
- artifact links into Explore
- a lightweight result preview for quick inspection
+The run header itself should also show an aggregated summary:
+
+- run duration
+- run-level task counts
+- total stdout/stderr line counts
+- failed task ids when present
+
## Screen 6: Explore Workspace
Purpose:
diff --git a/design/05-data/mongodb-data-model.md b/design/05-data/mongodb-data-model.md
index 20d333f..87fad10 100644
--- a/design/05-data/mongodb-data-model.md
+++ b/design/05-data/mongodb-data-model.md
@@ -299,6 +299,7 @@ Core fields:
- `summary`
- `startedAt`
- `finishedAt`
+- `durationMs`
- `createdAt`
### run_tasks
@@ -325,6 +326,8 @@ Core fields:
- `cacheKey`
- `cacheHit`
- `logLines`
+- `stdoutLines`
+- `stderrLines`
- `errorMessage`
- `summary`
- `lastResultPreview`
@@ -342,8 +345,17 @@ The current executable worker path expects `run_tasks` to be self-sufficient eno
- upstream node dependencies
- produced artifact ids
- per-task status and error message
-- task log lines and result preview
-- structured task summaries with executor, outcome, asset count, and artifact ids
+- task log lines, stdout/stderr streams, and result preview
+- structured task summaries with executor, outcome, asset count, artifact ids, and stdout/stderr counters
+
+The current runtime also aggregates task execution back onto `workflow_runs`, so run documents now carry:
+
+- task counts by status
+- completed task count
+- artifact count
+- total stdout/stderr line counts
+- failed task ids
+- derived run duration
### artifacts
diff --git a/docs/plans/2026-03-26-emboflow-v1-foundation-and-mvp.md b/docs/plans/2026-03-26-emboflow-v1-foundation-and-mvp.md
index a947221..f16defb 100644
--- a/docs/plans/2026-03-26-emboflow-v1-foundation-and-mvp.md
+++ b/docs/plans/2026-03-26-emboflow-v1-foundation-and-mvp.md
@@ -21,6 +21,7 @@
- `2026-03-26`: The current runtime pass binds workflow runs to registered project assets so run snapshots, run tasks, worker execution context, and the editor all agree on the concrete input asset being processed.
- `2026-03-26`: The current UI/runtime pass turns Runs into a real project-scoped workspace with run history queries, active-run polling, and task artifact links into Explore.
- `2026-03-27`: The current observability pass persists task execution summaries, timestamps, log lines, and result previews on Mongo-backed `run_tasks`, and surfaces those fields in the React run detail view.
+- `2026-03-27`: The current follow-up observability pass adds persisted stdout/stderr fields on `run_tasks` plus aggregated run summaries, durations, and task counts on `workflow_runs`.
---