diff --git a/apps/hash-ai-worker-ts/src/activities/flow-activities/generate-flow-run-name-activity.ts b/apps/hash-ai-worker-ts/src/activities/flow-activities/generate-flow-run-name-activity.ts
index d29b6b5a319..01df5f7e3dd 100644
--- a/apps/hash-ai-worker-ts/src/activities/flow-activities/generate-flow-run-name-activity.ts
+++ b/apps/hash-ai-worker-ts/src/activities/flow-activities/generate-flow-run-name-activity.ts
@@ -1,8 +1,7 @@
import {
- automaticBrowserInferenceFlow,
- manualBrowserInferenceFlow,
-} from "@local/hash-isomorphic-utils/flows/browser-plugin-flow-definitions";
-import { isGoalFlowDefinitionId } from "@local/hash-isomorphic-utils/flows/goal-flow-definitions";
+ flowDefinitionIds,
+ isGoalFlowDefinitionId,
+} from "@local/hash-isomorphic-utils/flows/flow-definition-ids";
import { getFlowContext } from "../shared/get-flow-context.js";
import { getLlmResponse } from "../shared/get-llm-response.js";
@@ -15,7 +14,7 @@ import type {
AutomaticInferenceInputName,
ManualInferenceInputName,
} from "@local/hash-isomorphic-utils/flows/browser-plugin-flow-types";
-import type { GoalFlowInputName } from "@local/hash-isomorphic-utils/flows/goal-flow-definitions";
+import type { GoalFlowInputName } from "@local/hash-isomorphic-utils/flows/definitions/goal-flow-definitions";
import type {
FlowActionDefinitionId,
FlowDefinition,
@@ -100,8 +99,8 @@ export const generateFlowRunName = async (
if (
[
- automaticBrowserInferenceFlow.flowDefinitionId,
- manualBrowserInferenceFlow.flowDefinitionId,
+ flowDefinitionIds.automaticBrowserInference,
+ flowDefinitionIds.manualBrowserInference,
].includes(flowDefinitionId)
) {
const webPage = flowInputs[
@@ -114,7 +113,7 @@ export const generateFlowRunName = async (
}
return `${
- flowDefinitionId === automaticBrowserInferenceFlow.flowDefinitionId
+ flowDefinitionId === flowDefinitionIds.automaticBrowserInference
? "Auto-analyze"
: "Analyze"
} webpage: ${webPage.url}`;
diff --git a/apps/hash-ai-worker-ts/src/activities/shared/get-flow-context.ts b/apps/hash-ai-worker-ts/src/activities/shared/get-flow-context.ts
index ae34bd03310..71de562e8c7 100644
--- a/apps/hash-ai-worker-ts/src/activities/shared/get-flow-context.ts
+++ b/apps/hash-ai-worker-ts/src/activities/shared/get-flow-context.ts
@@ -18,7 +18,7 @@ import { graphApiClient } from "./graph-api-client.js";
import type { EntityId, UserId, WebId } from "@blockprotocol/type-system";
import type { ManualInferenceInputName } from "@local/hash-isomorphic-utils/flows/browser-plugin-flow-types";
-import type { GoalFlowInputName } from "@local/hash-isomorphic-utils/flows/goal-flow-definitions";
+import type { GoalFlowInputName } from "@local/hash-isomorphic-utils/flows/definitions/goal-flow-definitions";
import type { RunAiFlowWorkflowParams } from "@local/hash-isomorphic-utils/flows/temporal-types";
import type { FlowDataSources } from "@local/hash-isomorphic-utils/flows/types";
import type { File } from "@local/hash-isomorphic-utils/system-types/shared";
diff --git a/apps/hash-api/src/ai/infer-entities-websocket/handle-infer-entities-request.ts b/apps/hash-api/src/ai/infer-entities-websocket/handle-infer-entities-request.ts
index 3f8b6c2e0b5..e556d53c9c1 100644
--- a/apps/hash-api/src/ai/infer-entities-websocket/handle-infer-entities-request.ts
+++ b/apps/hash-api/src/ai/infer-entities-websocket/handle-infer-entities-request.ts
@@ -1,8 +1,5 @@
import { getFlowRuns } from "@local/hash-backend-utils/flows";
-import {
- automaticBrowserInferenceFlow,
- manualBrowserInferenceFlow,
-} from "@local/hash-isomorphic-utils/flows/browser-plugin-flow-definitions";
+import { flowDefinitionIds } from "@local/hash-isomorphic-utils/flows/flow-definition-ids";
import { FlowRunStatus } from "../../graphql/api-types.gen";
@@ -44,10 +41,10 @@ export const handleInferEntitiesRequest = async ({
payload: { webId, ...flowInputs },
} = message;
- const { flowDefinitionId } =
+ const flowDefinitionId =
message.type === "manual-inference-request"
- ? manualBrowserInferenceFlow
- : automaticBrowserInferenceFlow;
+ ? flowDefinitionIds.manualBrowserInference
+ : flowDefinitionIds.automaticBrowserInference;
if (message.type === "automatic-inference-request") {
const openFlowRuns = await getFlowRuns({
@@ -55,8 +52,8 @@ export const handleInferEntitiesRequest = async ({
filters: {
executionStatus: FlowRunStatus.Running,
flowDefinitionIds: [
- automaticBrowserInferenceFlow.flowDefinitionId,
- manualBrowserInferenceFlow.flowDefinitionId,
+ flowDefinitionIds.automaticBrowserInference,
+ flowDefinitionIds.manualBrowserInference,
],
},
graphApiClient,
diff --git a/apps/hash-api/src/graph/ensure-system-graph-is-initialized/migrate-ontology-types/dev-flows.ts b/apps/hash-api/src/graph/ensure-system-graph-is-initialized/migrate-ontology-types/dev-flows.ts
index d0dd21e713b..ced081dce7f 100644
--- a/apps/hash-api/src/graph/ensure-system-graph-is-initialized/migrate-ontology-types/dev-flows.ts
+++ b/apps/hash-api/src/graph/ensure-system-graph-is-initialized/migrate-ontology-types/dev-flows.ts
@@ -1,7 +1,7 @@
import {
historicalFlightsFlow,
scheduledFlightsFlow,
-} from "@local/hash-isomorphic-utils/flows/integration-flow-definitions";
+} from "@local/hash-isomorphic-utils/flows/definitions/integration-flow-definitions";
import type { FlowDefinitionWithId } from "@local/hash-isomorphic-utils/flows/types";
diff --git a/apps/hash-api/src/graph/ensure-system-graph-is-initialized/migrate-ontology-types/seeded-flows.test.ts b/apps/hash-api/src/graph/ensure-system-graph-is-initialized/migrate-ontology-types/seeded-flows.test.ts
index 37b2530d983..b230e171833 100644
--- a/apps/hash-api/src/graph/ensure-system-graph-is-initialized/migrate-ontology-types/seeded-flows.test.ts
+++ b/apps/hash-api/src/graph/ensure-system-graph-is-initialized/migrate-ontology-types/seeded-flows.test.ts
@@ -1,11 +1,11 @@
import { describe, expect, it } from "vitest";
-import * as browserPluginFlows from "@local/hash-isomorphic-utils/flows/browser-plugin-flow-definitions";
-import * as fileFlows from "@local/hash-isomorphic-utils/flows/file-flow-definitions";
-import * as frontendFlows from "@local/hash-isomorphic-utils/flows/frontend-flow-definitions";
-import * as ftseInvestorsFlows from "@local/hash-isomorphic-utils/flows/ftse-investors-flow-definition";
-import * as goalFlows from "@local/hash-isomorphic-utils/flows/goal-flow-definitions";
-import * as integrationFlows from "@local/hash-isomorphic-utils/flows/integration-flow-definitions";
+import * as browserPluginFlows from "@local/hash-isomorphic-utils/flows/definitions/browser-plugin-flow-definitions";
+import * as fileFlows from "@local/hash-isomorphic-utils/flows/definitions/file-flow-definitions";
+import * as frontendFlows from "@local/hash-isomorphic-utils/flows/definitions/frontend-flow-definitions";
+import * as ftseInvestorsFlows from "@local/hash-isomorphic-utils/flows/definitions/ftse-investors-flow-definition";
+import * as goalFlows from "@local/hash-isomorphic-utils/flows/definitions/goal-flow-definitions";
+import * as integrationFlows from "@local/hash-isomorphic-utils/flows/definitions/integration-flow-definitions";
import { validateFlowDefinition } from "@local/hash-isomorphic-utils/flows/validate-flow-definition";
import { devFlows } from "./dev-flows";
diff --git a/apps/hash-api/src/graph/ensure-system-graph-is-initialized/migrate-ontology-types/seeded-flows.ts b/apps/hash-api/src/graph/ensure-system-graph-is-initialized/migrate-ontology-types/seeded-flows.ts
index 329575d26d8..c4dd2548107 100644
--- a/apps/hash-api/src/graph/ensure-system-graph-is-initialized/migrate-ontology-types/seeded-flows.ts
+++ b/apps/hash-api/src/graph/ensure-system-graph-is-initialized/migrate-ontology-types/seeded-flows.ts
@@ -1,13 +1,13 @@
import {
automaticBrowserInferenceFlow,
manualBrowserInferenceFlow,
-} from "@local/hash-isomorphic-utils/flows/browser-plugin-flow-definitions";
-import { inferMetadataFromDocumentFlow } from "@local/hash-isomorphic-utils/flows/file-flow-definitions";
+} from "@local/hash-isomorphic-utils/flows/definitions/browser-plugin-flow-definitions";
+import { inferMetadataFromDocumentFlow } from "@local/hash-isomorphic-utils/flows/definitions/file-flow-definitions";
import {
configureDashboardItemFlow,
refineDashboardItemFlow,
-} from "@local/hash-isomorphic-utils/flows/frontend-flow-definitions";
-import { goalFlow } from "@local/hash-isomorphic-utils/flows/goal-flow-definitions";
+} from "@local/hash-isomorphic-utils/flows/definitions/frontend-flow-definitions";
+import { goalFlow } from "@local/hash-isomorphic-utils/flows/definitions/goal-flow-definitions";
import type { FlowDefinitionWithId } from "@local/hash-isomorphic-utils/flows/types";
diff --git a/apps/hash-api/src/graphql/resolvers/knowledge/file/shared.ts b/apps/hash-api/src/graphql/resolvers/knowledge/file/shared.ts
index c2d2eef5dd0..7a09a7a4f8b 100644
--- a/apps/hash-api/src/graphql/resolvers/knowledge/file/shared.ts
+++ b/apps/hash-api/src/graphql/resolvers/knowledge/file/shared.ts
@@ -1,4 +1,4 @@
-import { inferMetadataFromDocumentFlow } from "@local/hash-isomorphic-utils/flows/file-flow-definitions";
+import { flowDefinitionIds } from "@local/hash-isomorphic-utils/flows/flow-definition-ids";
import { generateUuid } from "@local/hash-isomorphic-utils/generate-uuid";
import type { Entity, UserId, WebId } from "@blockprotocol/type-system";
@@ -42,7 +42,7 @@ export const triggerPdfAnalysisWorkflow = async ({
},
},
},
- flowDefinitionId: inferMetadataFromDocumentFlow.flowDefinitionId,
+ flowDefinitionId: flowDefinitionIds.inferMetadataFromDocument,
flowInputs: {
fileEntityId: { kind: "EntityId", value: entityId },
},
@@ -58,7 +58,7 @@ export const triggerPdfAnalysisWorkflow = async ({
taskQueue: "ai",
args: [params],
memo: {
- flowDefinitionId: inferMetadataFromDocumentFlow.flowDefinitionId,
+ flowDefinitionId: flowDefinitionIds.inferMetadataFromDocument,
userAccountId,
webId,
},
diff --git a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer.tsx b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer.tsx
index 7e313a1d427..667db745d89 100644
--- a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer.tsx
+++ b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer.tsx
@@ -10,12 +10,10 @@ import { IconButton, Skeleton } from "@hashintel/design-system";
import { deserializeSubgraph } from "@local/hash-graph-sdk/subgraph";
import { actionDefinitions } from "@local/hash-isomorphic-utils/flows/action-definitions";
import {
- automaticBrowserInferenceFlow,
- manualBrowserInferenceFlow,
-} from "@local/hash-isomorphic-utils/flows/browser-plugin-flow-definitions";
-import { inferMetadataFromDocumentFlow } from "@local/hash-isomorphic-utils/flows/file-flow-definitions";
+ flowDefinitionIds,
+ isGoalFlowDefinitionId,
+} from "@local/hash-isomorphic-utils/flows/flow-definition-ids";
import { generateWorkerRunPath } from "@local/hash-isomorphic-utils/flows/frontend-paths";
-import { isGoalFlowDefinitionId } from "@local/hash-isomorphic-utils/flows/goal-flow-definitions";
import {
almostFullOntologyResolveDepths,
currentTimeInstantTemporalAxes,
@@ -36,6 +34,7 @@ import { defaultBrowserPluginDomains } from "../../../goals/new.page/default-bro
import { useFlowDefinitionsContext } from "../../../shared/flow-definitions-context";
import { useFlowRunsContext } from "../../../shared/flow-runs-context";
import { NotFound } from "../../../shared/not-found";
+import { useFlowDefinitionAtTime } from "../../../shared/use-flow-definitions";
import { useFlowSchedules } from "../../../shared/use-flow-schedules";
import { ActivityLog } from "./flow-visualizer/activity-log";
import { DAG } from "./flow-visualizer/dag";
@@ -64,7 +63,12 @@ import type {
ProposedEntityOutput,
} from "./flow-visualizer/shared/types";
import type { EntityRootType, Subgraph } from "@blockprotocol/graph";
-import type { EntityId, WebId } from "@blockprotocol/type-system";
+import type {
+ EntityId,
+ EntityUuid,
+ Timestamp,
+ WebId,
+} from "@blockprotocol/type-system";
import type { Filter } from "@local/hash-graph-client";
import type { HashEntity } from "@local/hash-graph-sdk/entity";
import type {
@@ -167,9 +171,9 @@ const containerHeight = `calc(100vh - ${HEADER_HEIGHT}px)`;
/** Flows that only the browser plugin or a file upload starts, because their inputs come from there. */
const unrunnableDefinitionIds = [
- manualBrowserInferenceFlow.flowDefinitionId,
- automaticBrowserInferenceFlow.flowDefinitionId,
- inferMetadataFromDocumentFlow.flowDefinitionId,
+ flowDefinitionIds.manualBrowserInference,
+ flowDefinitionIds.automaticBrowserInference,
+ flowDefinitionIds.inferMetadataFromDocument,
];
export const FlowRunVisualizerSkeleton = () => (
@@ -207,6 +211,21 @@ export const FlowVisualizer = () => {
const { selectedFlowRun } = useFlowRunsContext();
+ /**
+ * A run is drawn with the edition of its definition it executed: the one current when it started.
+ */
+ const {
+ flowDefinition: flowOfSelectedRun,
+ loading: flowOfSelectedRunLoading,
+ } = useFlowDefinitionAtTime({
+ flowDefinitionId: selectedFlowRun
+ ? (selectedFlowRun.flowDefinitionId as EntityUuid)
+ : null,
+ timestamp: selectedFlowRun
+ ? (selectedFlowRun.startedAt as Timestamp)
+ : null,
+ });
+
const { schedulesByEntityUuid } = useFlowSchedules();
/**
@@ -224,12 +243,18 @@ export const FlowVisualizer = () => {
const selectedFlow = useMemo(
() =>
- flowDefinitions.find(
- ({ flowDefinitionId }) =>
- flowDefinitionId ===
- (selectedFlowRun?.flowDefinitionId ?? selectedFlowDefinitionId),
- ),
- [flowDefinitions, selectedFlowDefinitionId, selectedFlowRun],
+ selectedFlowRun
+ ? flowOfSelectedRun
+ : flowDefinitions.find(
+ ({ flowDefinitionId }) =>
+ flowDefinitionId === selectedFlowDefinitionId,
+ ),
+ [
+ flowDefinitions,
+ flowOfSelectedRun,
+ selectedFlowDefinitionId,
+ selectedFlowRun,
+ ],
);
const selectedFlowDefinition = selectedFlow?.flowDefinition;
@@ -633,7 +658,7 @@ export const FlowVisualizer = () => {
}, [runFlow, selectedFlowRun]);
if (!selectedFlow || !selectedFlowDefinition) {
- if (flowDefinitionsLoading) {
+ if (flowDefinitionsLoading || flowOfSelectedRunLoading) {
return ;
}
diff --git a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/dag-slide.tsx b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/dag-slide.tsx
index 8beef2d6d8f..ef393b6c4a3 100644
--- a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/dag-slide.tsx
+++ b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/dag-slide.tsx
@@ -1,7 +1,7 @@
import { Backdrop, Box, Slide, Stack } from "@mui/material";
import { IconButton, XMarkRegularIcon } from "@hashintel/design-system";
-import { isGoalFlowDefinitionId } from "@local/hash-isomorphic-utils/flows/goal-flow-definitions";
+import { isGoalFlowDefinitionId } from "@local/hash-isomorphic-utils/flows/flow-definition-ids";
import { DAG } from "./dag";
import { Topbar } from "./topbar";
diff --git a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/flow-run-sidebar.tsx b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/flow-run-sidebar.tsx
index ef523fbe993..1ac052f4625 100644
--- a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/flow-run-sidebar.tsx
+++ b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/flow-run-sidebar.tsx
@@ -7,10 +7,7 @@ import {
ClockRegularIcon,
IconButton,
} from "@hashintel/design-system";
-import {
- isGoalFlowDefinitionId,
- type GoalFlowInputName,
-} from "@local/hash-isomorphic-utils/flows/goal-flow-definitions";
+import { isGoalFlowDefinitionId } from "@local/hash-isomorphic-utils/flows/flow-definition-ids";
import { Link } from "../../../../../shared/ui/link";
import { useFlowRunsContext } from "../../../../shared/flow-runs-context";
@@ -23,6 +20,7 @@ import { flowSectionBorderRadius } from "./shared/styles";
import type { FlowRun } from "../../../../../graphql/api-types.gen";
import type { EdgesAndNodes } from "./shared/types";
import type { EntityId, EntityUuid } from "@blockprotocol/type-system";
+import type { GoalFlowInputName } from "@local/hash-isomorphic-utils/flows/definitions/goal-flow-definitions";
import type { PropsWithChildren } from "react";
const SidebarSection = ({ children }: PropsWithChildren) => (
diff --git a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/flow-run-sidebar/group-status.tsx b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/flow-run-sidebar/group-status.tsx
index 38d89496893..f8b77c3b555 100644
--- a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/flow-run-sidebar/group-status.tsx
+++ b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/flow-run-sidebar/group-status.tsx
@@ -17,7 +17,12 @@ import type { EdgesAndNodes } from "../shared/types";
export const GroupStatus = ({ graph }: { graph: EdgesAndNodes }) => {
const groupStepsWithIds = useMemo(
- () => graph.nodes.map((node) => ({ ...node, stepId: node.id })),
+ () =>
+ graph.nodes.map((node) => ({
+ ...node,
+ stepId: node.id,
+ parentStepId: node.parentNode,
+ })),
[graph],
);
diff --git a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/outputs.tsx b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/outputs.tsx
index bdcea06b79c..40c3b409f03 100644
--- a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/outputs.tsx
+++ b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/outputs.tsx
@@ -9,7 +9,7 @@ import {
} from "@blockprotocol/type-system";
import { CheckRegularIcon, IconButton } from "@hashintel/design-system";
import { HashEntity } from "@local/hash-graph-sdk/entity";
-import { isGoalFlowDefinitionId } from "@local/hash-isomorphic-utils/flows/goal-flow-definitions";
+import { isGoalFlowDefinitionId } from "@local/hash-isomorphic-utils/flows/flow-definition-ids";
import { currentTimeInstantTemporalAxes } from "@local/hash-isomorphic-utils/graph-queries";
import { getClosedMultiEntityTypesQuery } from "../../../../../graphql/queries/ontology/entity-type.queries";
diff --git a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/swimlane.tsx b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/swimlane.tsx
index e441232ec64..84f8db3710e 100644
--- a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/swimlane.tsx
+++ b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/swimlane.tsx
@@ -139,7 +139,8 @@ export const Swimlane = ({
);
const stepsWithIds = useMemo(
- () => nodes.map((node) => ({ stepId: node.id })),
+ () =>
+ nodes.map((node) => ({ stepId: node.id, parentStepId: node.parentNode })),
[nodes],
);
diff --git a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/swimlane/custom-node.tsx b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/swimlane/custom-node.tsx
index f0cb27c7a04..0758ab1f4d6 100644
--- a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/swimlane/custom-node.tsx
+++ b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/swimlane/custom-node.tsx
@@ -3,6 +3,7 @@ import { formatDistance } from "date-fns";
import { useEffect, useMemo, useState } from "react";
import { ArrowRightRegularIcon } from "@hashintel/design-system";
+import { parseStepRunId } from "@local/hash-isomorphic-utils/flows/step-run-id";
import { Button } from "../../../../../../shared/ui/button";
import {
@@ -119,7 +120,8 @@ export const CustomNode = ({ data, id, selected }: NodeProps) => {
*/
let browserInputRequest: ExternalInputRequest | undefined = undefined;
for (const request of selectedFlowRun?.inputRequests ?? []) {
- if (request.resolvedAt ?? request.stepId !== id) {
+ /* A request is raised by a step run, which inside a for-each is one of several runs of this node's step. */
+ if (request.resolvedAt ?? parseStepRunId(request.stepId).stepId !== id) {
continue;
}
if (request.type === "human-input") {
diff --git a/apps/hash-frontend/src/pages/dashboard/hooks/use-dashboard-item-config.ts b/apps/hash-frontend/src/pages/dashboard/hooks/use-dashboard-item-config.ts
index 24c195176c3..bb77e7c4606 100644
--- a/apps/hash-frontend/src/pages/dashboard/hooks/use-dashboard-item-config.ts
+++ b/apps/hash-frontend/src/pages/dashboard/hooks/use-dashboard-item-config.ts
@@ -5,10 +5,7 @@ import {
getPrimaryChartType,
normalizeStructuralQuery,
} from "@local/hash-isomorphic-utils/dashboard-types";
-import {
- configureDashboardItemFlow,
- refineDashboardItemFlow,
-} from "@local/hash-isomorphic-utils/flows/frontend-flow-definitions";
+import { flowDefinitionIds } from "@local/hash-isomorphic-utils/flows/flow-definition-ids";
import { getFlowRunById } from "@local/hash-isomorphic-utils/graphql/queries/flow.queries";
import { systemPropertyTypes } from "@local/hash-isomorphic-utils/ontology-type-ids";
@@ -603,7 +600,7 @@ export const useDashboardItemConfig = ({
// Start the flow
const { data } = await startFlow({
variables: {
- flowDefinitionId: configureDashboardItemFlow.flowDefinitionId,
+ flowDefinitionId: flowDefinitionIds.configureDashboardItem,
flowInputs: {
userGoal: {
kind: "Text",
@@ -721,7 +718,7 @@ export const useDashboardItemConfig = ({
const { data } = await startFlow({
variables: {
- flowDefinitionId: refineDashboardItemFlow.flowDefinitionId,
+ flowDefinitionId: flowDefinitionIds.refineDashboardItem,
flowInputs: {
userGoal: { kind: "Text", value: initialGoal },
refinementInstruction: {
diff --git a/apps/hash-frontend/src/pages/flows.page.tsx b/apps/hash-frontend/src/pages/flows.page.tsx
index db2c725ccf8..bc221aa18ce 100644
--- a/apps/hash-frontend/src/pages/flows.page.tsx
+++ b/apps/hash-frontend/src/pages/flows.page.tsx
@@ -3,8 +3,8 @@ import { formatDistanceToNowStrict } from "date-fns";
import { memo, useMemo, useState } from "react";
import { InfinityLightIcon } from "@hashintel/design-system";
+import { isGoalFlowDefinitionId } from "@local/hash-isomorphic-utils/flows/flow-definition-ids";
import { generateFlowDefinitionPath } from "@local/hash-isomorphic-utils/flows/frontend-paths";
-import { isGoalFlowDefinitionId } from "@local/hash-isomorphic-utils/flows/goal-flow-definitions";
import { getLayoutWithSidebar } from "../shared/layout";
import { Link } from "../shared/ui/link";
diff --git a/apps/hash-frontend/src/pages/goals.page/goals-list.tsx b/apps/hash-frontend/src/pages/goals.page/goals-list.tsx
index 9ea6ca7c1a3..b502f4b1315 100644
--- a/apps/hash-frontend/src/pages/goals.page/goals-list.tsx
+++ b/apps/hash-frontend/src/pages/goals.page/goals-list.tsx
@@ -1,7 +1,7 @@
import { Box } from "@mui/material";
import { useMemo } from "react";
-import { isGoalFlowDefinitionId } from "@local/hash-isomorphic-utils/flows/goal-flow-definitions";
+import { isGoalFlowDefinitionId } from "@local/hash-isomorphic-utils/flows/flow-definition-ids";
import { useAuthenticatedUser } from "../shared/auth-info-context";
import { useFlowRunsContext } from "../shared/flow-runs-context";
diff --git a/apps/hash-frontend/src/pages/goals/new.page.tsx b/apps/hash-frontend/src/pages/goals/new.page.tsx
index cc0c6dcb345..3f26caf3980 100644
--- a/apps/hash-frontend/src/pages/goals/new.page.tsx
+++ b/apps/hash-frontend/src/pages/goals/new.page.tsx
@@ -18,11 +18,8 @@ import {
BullseyeLightIcon,
TextField,
} from "@hashintel/design-system";
+import { flowDefinitionIds } from "@local/hash-isomorphic-utils/flows/flow-definition-ids";
import { generateWorkerRunPath } from "@local/hash-isomorphic-utils/flows/frontend-paths";
-import {
- goalFlow,
- type GoalFlowInputName,
-} from "@local/hash-isomorphic-utils/flows/goal-flow-definitions";
import { getFlowRunsQuery } from "@local/hash-isomorphic-utils/graphql/queries/flow.queries";
import { useGetOwnerForEntity } from "../../components/hooks/use-get-owner-for-entity";
@@ -48,6 +45,7 @@ import type { NextPageWithLayout } from "../../shared/layout";
import type { DeliverableSettingsState } from "./new.page/deliverable-settings";
import type { FileSettingsState } from "./new.page/file-settings";
import type { EntityTypeWithMetadata, WebId } from "@blockprotocol/type-system";
+import type { GoalFlowInputName } from "@local/hash-isomorphic-utils/flows/definitions/goal-flow-definitions";
import type {
FlowDataSources,
Payload,
@@ -286,7 +284,7 @@ const NewGoalPageContent = () => {
},
internetAccess: internetSettings,
},
- flowDefinitionId: goalFlow.flowDefinitionId,
+ flowDefinitionId: flowDefinitionIds.goal,
flowInputs,
webId,
},
diff --git a/apps/hash-frontend/src/pages/shared/flow-definitions-context-provider.tsx b/apps/hash-frontend/src/pages/shared/flow-definitions-context-provider.tsx
index 9e8d8090721..b5ff0cae3c4 100644
--- a/apps/hash-frontend/src/pages/shared/flow-definitions-context-provider.tsx
+++ b/apps/hash-frontend/src/pages/shared/flow-definitions-context-provider.tsx
@@ -1,51 +1,25 @@
import { useMemo } from "react";
-import {
- automaticBrowserInferenceFlow,
- manualBrowserInferenceFlow,
-} from "@local/hash-isomorphic-utils/flows/browser-plugin-flow-definitions";
-import { inferMetadataFromDocumentFlow } from "@local/hash-isomorphic-utils/flows/file-flow-definitions";
-import {
- configureDashboardItemFlow,
- refineDashboardItemFlow,
-} from "@local/hash-isomorphic-utils/flows/frontend-flow-definitions";
-import { ftseInvestorsFlow } from "@local/hash-isomorphic-utils/flows/ftse-investors-flow-definition";
-import { goalFlow } from "@local/hash-isomorphic-utils/flows/goal-flow-definitions";
-import {
- historicalFlightsFlow,
- scheduledFlightsFlow,
-} from "@local/hash-isomorphic-utils/flows/integration-flow-definitions";
-
import { FlowDefinitionsContext } from "./flow-definitions-context";
+import { useFlowDefinitions } from "./use-flow-definitions";
import type { FlowDefinitionsContextType } from "./flow-definitions-context";
import type { EntityUuid } from "@blockprotocol/type-system";
-import type { FlowDefinitionWithId } from "@local/hash-isomorphic-utils/flows/types";
import type { PropsWithChildren } from "react";
-const flowsDefinedInCode: FlowDefinitionWithId[] = [
- goalFlow,
- manualBrowserInferenceFlow,
- automaticBrowserInferenceFlow,
- inferMetadataFromDocumentFlow,
- configureDashboardItemFlow,
- refineDashboardItemFlow,
- historicalFlightsFlow,
- scheduledFlightsFlow,
- ftseInvestorsFlow,
-];
-
export const FlowDefinitionsContextProvider = ({
children,
selectedFlowDefinitionId,
}: PropsWithChildren<{ selectedFlowDefinitionId: EntityUuid | null }>) => {
+ const { flowDefinitions, loading } = useFlowDefinitions();
+
const context = useMemo(
() => ({
- flowDefinitions: flowsDefinedInCode,
- loading: false,
+ flowDefinitions,
+ loading,
selectedFlowDefinitionId,
}),
- [selectedFlowDefinitionId],
+ [flowDefinitions, loading, selectedFlowDefinitionId],
);
return (
diff --git a/apps/hash-frontend/src/pages/shared/flow-definitions-context.ts b/apps/hash-frontend/src/pages/shared/flow-definitions-context.ts
index d10425d1625..a9b9ca439ff 100644
--- a/apps/hash-frontend/src/pages/shared/flow-definitions-context.ts
+++ b/apps/hash-frontend/src/pages/shared/flow-definitions-context.ts
@@ -4,7 +4,7 @@ import type { EntityUuid } from "@blockprotocol/type-system";
import type { FlowDefinitionWithId } from "@local/hash-isomorphic-utils/flows/types";
export type FlowDefinitionsContextType = {
- /** Every flow defined in code. */
+ /** The current edition of every flow definition the user can view, from the graph. */
flowDefinitions: FlowDefinitionWithId[];
loading: boolean;
selectedFlowDefinitionId: EntityUuid | null;
diff --git a/apps/hash-frontend/src/pages/shared/flow-runs-context.test.ts b/apps/hash-frontend/src/pages/shared/flow-runs-context.test.ts
new file mode 100644
index 00000000000..a0fc788c24b
--- /dev/null
+++ b/apps/hash-frontend/src/pages/shared/flow-runs-context.test.ts
@@ -0,0 +1,84 @@
+import { describe, expect, it } from "vitest";
+
+import { generateStepRunId } from "@local/hash-isomorphic-utils/flows/step-run-id";
+
+import { FlowRunStatus, FlowStepStatus } from "../../graphql/api-types.gen";
+import { getShownStepRun, getStatusForSteps } from "./flow-runs-context";
+
+import type { StepRun } from "../../graphql/api-types.gen";
+
+const stepRun = (stepId: string, status: FlowStepStatus): StepRun => ({
+ attempt: 1,
+ logs: [],
+ scheduledAt: "2026-01-01T00:00:00.000Z",
+ status,
+ stepId,
+ stepType: "action",
+});
+
+/** A step before a `for-each`, and a step inside it, with a run per item. */
+const steps = [
+ { stepId: "fetch" },
+ { stepId: "forEach" },
+ { stepId: "summarise", parentStepId: "forEach" },
+];
+
+describe("getStatusForSteps", () => {
+ it("counts the runs of a step inside a for-each towards it and the for-each, until every item's run is complete", () => {
+ const running = getStatusForSteps(
+ {
+ closedAt: null,
+ status: FlowRunStatus.Running,
+ steps: [
+ stepRun("fetch", FlowStepStatus.Completed),
+ stepRun(generateStepRunId("summarise", 0), FlowStepStatus.Completed),
+ stepRun(generateStepRunId("summarise", 1), FlowStepStatus.Started),
+ ],
+ },
+ steps,
+ );
+
+ expect(running.overallStatus).toBe("In Progress");
+ expect(running.statusByStep).toEqual({
+ fetch: "Complete",
+ forEach: "In Progress",
+ summarise: "In Progress",
+ });
+
+ const complete = getStatusForSteps(
+ {
+ closedAt: null,
+ status: FlowRunStatus.Running,
+ steps: [
+ stepRun("fetch", FlowStepStatus.Completed),
+ stepRun(generateStepRunId("summarise", 0), FlowStepStatus.Completed),
+ stepRun(generateStepRunId("summarise", 1), FlowStepStatus.Completed),
+ ],
+ },
+ steps,
+ );
+
+ expect(complete.overallStatus).toBe("Complete");
+ expect(complete.statusByStep.summarise).toBe("Complete");
+ });
+});
+
+describe("getShownStepRun", () => {
+ it("shows the run of a step inside a for-each whose status most needs attention", () => {
+ const failed = stepRun(
+ generateStepRunId("summarise", 1),
+ FlowStepStatus.Failed,
+ );
+
+ expect(
+ getShownStepRun(
+ [
+ stepRun(generateStepRunId("summarise", 0), FlowStepStatus.Completed),
+ failed,
+ stepRun(generateStepRunId("summarise", 2), FlowStepStatus.Started),
+ ],
+ "summarise",
+ ),
+ ).toBe(failed);
+ });
+});
diff --git a/apps/hash-frontend/src/pages/shared/flow-runs-context.ts b/apps/hash-frontend/src/pages/shared/flow-runs-context.ts
index dba4b9634ab..57b9ecd92d2 100644
--- a/apps/hash-frontend/src/pages/shared/flow-runs-context.ts
+++ b/apps/hash-frontend/src/pages/shared/flow-runs-context.ts
@@ -1,6 +1,8 @@
import { createContext, useContext, useMemo } from "react";
import { useNodeId } from "reactflow";
+import { parseStepRunId } from "@local/hash-isomorphic-utils/flows/step-run-id";
+
import { FlowRunStatus, FlowStepStatus } from "../../graphql/api-types.gen";
import type {
@@ -45,30 +47,6 @@ export type StepRunStatus = Pick<
"inputs" | "outputs" | "status" | "closedAt" | "scheduledAt" | "logs"
>;
-export const useStatusForStep = (
- nodeId: string | null,
-): StepRunStatus | null => {
- const { selectedFlowRun } = useFlowRunsContext();
-
- return useMemo(() => {
- if (!selectedFlowRun || !nodeId) {
- return null;
- }
-
- if (nodeId === "trigger") {
- return null;
- }
-
- return selectedFlowRun.steps.find((step) => step.stepId === nodeId) ?? null;
- }, [selectedFlowRun, nodeId]);
-};
-
-export const useStatusForCurrentStep = (): StepRunStatus | null => {
- const nodeId = useNodeId();
-
- return useStatusForStep(nodeId);
-};
-
export type SimpleStatus =
| "Waiting"
| "Information Required"
@@ -109,105 +87,196 @@ export const statusToSimpleStatus = (
return simpleStatus;
};
-export const useStatusForSteps = (
- steps: { stepId: string }[],
-): {
- closedAt?: string;
- scheduledAt?: string;
- overallStatus: SimpleStatus;
- statusByStep: Record;
-} | null => {
+/**
+ * The id of the step in the flow definition that a step run is of. The engine runs the steps inside a `for-each` once
+ * per item, each with its own step run id.
+ */
+const getDefinitionStepId = (stepRunId: string) =>
+ parseStepRunId(stepRunId).stepId;
+
+/**
+ * The status shown for a step that has several runs, as a step inside a `for-each` does: the first in this list that
+ * any of its runs has.
+ */
+const statusPrecedence: SimpleStatus[] = [
+ "Errored",
+ "Information Required",
+ "In Progress",
+ "Waiting",
+ "Cancelled",
+ "Complete",
+ "Skipped",
+];
+
+const combineStatuses = (
+ status: SimpleStatus | undefined,
+ otherStatus: SimpleStatus,
+): SimpleStatus =>
+ status !== undefined &&
+ statusPrecedence.indexOf(status) <= statusPrecedence.indexOf(otherStatus)
+ ? status
+ : otherStatus;
+
+/**
+ * The run of a step, or for a step inside a `for-each`, the run of it whose status is shown (see `statusPrecedence`).
+ */
+export const getShownStepRun = (
+ stepRuns: Run[],
+ stepId: string,
+): Run | null =>
+ stepRuns
+ .filter((stepRun) => getDefinitionStepId(stepRun.stepId) === stepId)
+ .reduce(
+ (shownStepRun, stepRun) =>
+ !shownStepRun ||
+ statusPrecedence.indexOf(statusToSimpleStatus(stepRun.status)) <
+ statusPrecedence.indexOf(statusToSimpleStatus(shownStepRun.status))
+ ? stepRun
+ : shownStepRun,
+ null,
+ );
+
+export const useStatusForStep = (
+ nodeId: string | null,
+): StepRunStatus | null => {
const { selectedFlowRun } = useFlowRunsContext();
return useMemo(() => {
- const statusByStep: Record = {};
+ if (!selectedFlowRun || !nodeId) {
+ return null;
+ }
- if (!selectedFlowRun) {
+ if (nodeId === "trigger") {
return null;
}
- const stepRuns = selectedFlowRun.steps.filter((stepRun) =>
- steps.find((step) => step.stepId === stepRun.stepId),
- );
+ return getShownStepRun(selectedFlowRun.steps, nodeId);
+ }, [selectedFlowRun, nodeId]);
+};
- const flowCompletedUnsuccessfully =
- selectedFlowRun.closedAt &&
- selectedFlowRun.status !== FlowRunStatus.Completed;
+export const useStatusForCurrentStep = (): StepRunStatus | null => {
+ const nodeId = useNodeId();
+
+ return useStatusForStep(nodeId);
+};
+
+type StepsStatus = {
+ closedAt?: string;
+ scheduledAt?: string;
+ overallStatus: SimpleStatus;
+ statusByStep: Record;
+};
+
+/**
+ * The status of each of `steps` in a flow run, and of them together. A step inside a `for-each` (`parentStepId`) has a
+ * run per item: those count towards it and towards the `for-each`, which has no run of its own.
+ */
+export const getStatusForSteps = (
+ flowRun: Pick,
+ steps: { stepId: string; parentStepId?: string }[],
+): StepsStatus => {
+ const statusByStep: Record = {};
+
+ const stepsById = new Map(steps.map((step) => [step.stepId, step]));
+
+ const stepRuns = flowRun.steps.filter((stepRun) =>
+ stepsById.has(getDefinitionStepId(stepRun.stepId)),
+ );
+
+ const flowCompletedUnsuccessfully =
+ flowRun.closedAt && flowRun.status !== FlowRunStatus.Completed;
+
+ if (stepRuns.length === 0) {
+ return {
+ overallStatus: flowCompletedUnsuccessfully ? "Cancelled" : "Waiting",
+ statusByStep,
+ };
+ }
- if (stepRuns.length === 0) {
- return {
- overallStatus: flowCompletedUnsuccessfully ? "Cancelled" : "Waiting",
- statusByStep,
- };
+ let scheduledAt: string | undefined;
+ let latestClosedAt: string | undefined;
+ let allStepsClosed = true;
+ let status: SimpleStatus =
+ stepRuns.at(-1)!.status === FlowStepStatus.Cancelled
+ ? "Cancelled"
+ : "Complete";
+
+ let hasError: boolean = false;
+ let hasWaiting: boolean = false;
+ let hasInProgress: boolean = false;
+
+ for (const stepRun of stepRuns) {
+ if (!scheduledAt || stepRun.scheduledAt < scheduledAt) {
+ scheduledAt = stepRun.scheduledAt;
}
- let scheduledAt: string | undefined;
- let latestClosedAt: string | undefined;
- let allStepsClosed = true;
- let status: SimpleStatus =
- stepRuns.at(-1)!.status === FlowStepStatus.Cancelled
- ? "Cancelled"
- : "Complete";
-
- let hasError: boolean = false;
- let hasWaiting: boolean = false;
- let hasInProgress: boolean = false;
-
- for (const stepRun of stepRuns) {
- if (!scheduledAt || stepRun.scheduledAt < scheduledAt) {
- scheduledAt = stepRun.scheduledAt;
+ if (stepRun.closedAt) {
+ if (!latestClosedAt || stepRun.closedAt > latestClosedAt) {
+ latestClosedAt = stepRun.closedAt;
}
+ } else {
+ allStepsClosed = false;
+ }
- if (stepRun.closedAt) {
- if (!latestClosedAt || stepRun.closedAt > latestClosedAt) {
- latestClosedAt = stepRun.closedAt;
- }
- } else {
- allStepsClosed = false;
- }
+ let simpleStatus = statusToSimpleStatus(stepRun.status);
- let simpleStatus = statusToSimpleStatus(stepRun.status);
+ if (
+ flowRun.closedAt &&
+ ["In Progress", "Waiting", "Information Required"].includes(simpleStatus)
+ ) {
+ simpleStatus = "Cancelled";
+ }
- if (
- selectedFlowRun.closedAt &&
- ["In Progress", "Waiting", "Information Required"].includes(
- simpleStatus,
- )
- ) {
- simpleStatus = "Cancelled";
- }
+ const step = stepsById.get(getDefinitionStepId(stepRun.stepId))!;
- statusByStep[stepRun.stepId] = simpleStatus;
-
- if (simpleStatus === "Errored") {
- hasError = true;
- } else if (
- simpleStatus === "In Progress" ||
- simpleStatus === "Information Required"
- ) {
- hasInProgress = true;
- } else if (simpleStatus === "Waiting") {
- hasWaiting = true;
+ for (const stepId of [step.stepId, step.parentStepId]) {
+ if (stepId !== undefined) {
+ statusByStep[stepId] = combineStatuses(
+ statusByStep[stepId],
+ simpleStatus,
+ );
}
}
- if (hasError) {
- status = "Errored";
- } else if (hasInProgress) {
- status = "In Progress";
- } else if (hasWaiting) {
- status = "Waiting";
+ if (simpleStatus === "Errored") {
+ hasError = true;
+ } else if (
+ simpleStatus === "In Progress" ||
+ simpleStatus === "Information Required"
+ ) {
+ hasInProgress = true;
+ } else if (simpleStatus === "Waiting") {
+ hasWaiting = true;
}
+ }
- if (flowCompletedUnsuccessfully) {
- status = "Cancelled";
- }
+ if (hasError) {
+ status = "Errored";
+ } else if (hasInProgress) {
+ status = "In Progress";
+ } else if (hasWaiting) {
+ status = "Waiting";
+ }
- return {
- closedAt: allStepsClosed ? latestClosedAt : undefined,
- scheduledAt,
- overallStatus: status,
- statusByStep,
- };
- }, [selectedFlowRun, steps]);
+ if (flowCompletedUnsuccessfully) {
+ status = "Cancelled";
+ }
+
+ return {
+ closedAt: allStepsClosed ? latestClosedAt : undefined,
+ scheduledAt,
+ overallStatus: status,
+ statusByStep,
+ };
+};
+
+export const useStatusForSteps = (
+ steps: { stepId: string; parentStepId?: string }[],
+): StepsStatus | null => {
+ const { selectedFlowRun } = useFlowRunsContext();
+
+ return useMemo(
+ () => (selectedFlowRun ? getStatusForSteps(selectedFlowRun, steps) : null),
+ [selectedFlowRun, steps],
+ );
};
diff --git a/apps/hash-frontend/src/pages/shared/use-flow-definitions.ts b/apps/hash-frontend/src/pages/shared/use-flow-definitions.ts
new file mode 100644
index 00000000000..6f1a8aa9ca7
--- /dev/null
+++ b/apps/hash-frontend/src/pages/shared/use-flow-definitions.ts
@@ -0,0 +1,146 @@
+import { useQuery } from "@apollo/client";
+import { useMemo } from "react";
+
+import { getRoots } from "@blockprotocol/graph/stdlib";
+import { extractEntityUuidFromEntityId } from "@blockprotocol/type-system";
+import { deserializeSubgraph } from "@local/hash-graph-sdk/subgraph";
+import { getFlowDefinitionFromEntity } from "@local/hash-isomorphic-utils/flows/mappings";
+import {
+ currentTimeInstantTemporalAxes,
+ generateTimeInstantTemporalAxes,
+ zeroedOntologyResolveDepths,
+} from "@local/hash-isomorphic-utils/graph-queries";
+import { systemEntityTypes } from "@local/hash-isomorphic-utils/ontology-type-ids";
+
+import { queryEntitySubgraphQuery } from "../../graphql/queries/knowledge/entity.queries";
+
+import type {
+ QueryEntitySubgraphQuery,
+ QueryEntitySubgraphQueryVariables,
+} from "../../graphql/api-types.gen";
+import type { EntityRootType } from "@blockprotocol/graph";
+import type { EntityUuid, Timestamp } from "@blockprotocol/type-system";
+import type {
+ Filter,
+ QueryTemporalAxesUnresolved,
+} from "@local/hash-graph-client";
+import type { HashEntity } from "@local/hash-graph-sdk/entity";
+import type { FlowDefinitionWithId } from "@local/hash-isomorphic-utils/flows/types";
+import type { FlowDefinition as FlowDefinitionEntity } from "@local/hash-isomorphic-utils/system-types/flowdefinition";
+
+const flowDefinitionsQueryVariables = (
+ filter: Filter,
+ temporalAxes: QueryTemporalAxesUnresolved,
+): QueryEntitySubgraphQueryVariables => ({
+ request: {
+ filter: {
+ all: [
+ filter,
+ {
+ equal: [
+ { path: ["type", "baseUrl"] },
+ { parameter: systemEntityTypes.flowDefinition.entityTypeBaseUrl },
+ ],
+ },
+ { equal: [{ path: ["archived"] }, { parameter: false }] },
+ ],
+ },
+ graphResolveDepths: zeroedOntologyResolveDepths,
+ traversalPaths: [],
+ temporalAxes,
+ includeDrafts: false,
+ includePermissions: false,
+ },
+});
+
+/**
+ * The flow definitions the `Flow Definition` entities in a query's result hold. An entity whose definition is
+ * invalid is left out, with an error logged.
+ */
+const getFlowDefinitionsFromQueryResult = (
+ data: QueryEntitySubgraphQuery | undefined,
+): FlowDefinitionWithId[] => {
+ if (!data) {
+ return [];
+ }
+
+ const subgraph = deserializeSubgraph<
+ EntityRootType>
+ >(data.queryEntitySubgraph.subgraph);
+
+ return getRoots(subgraph).flatMap((entity) => {
+ const flowDefinitionId = extractEntityUuidFromEntityId(
+ entity.metadata.recordId.entityId,
+ );
+
+ try {
+ return {
+ flowDefinitionId,
+ flowDefinition: getFlowDefinitionFromEntity(entity),
+ };
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error(
+ `Flow definition ${flowDefinitionId} is invalid, so isn't shown: ${(error as Error).message}`,
+ );
+ return [];
+ }
+ });
+};
+
+/**
+ * The current edition of every flow definition the user can view.
+ */
+export const useFlowDefinitions = (): {
+ flowDefinitions: FlowDefinitionWithId[];
+ loading: boolean;
+} => {
+ const { data, loading } = useQuery<
+ QueryEntitySubgraphQuery,
+ QueryEntitySubgraphQueryVariables
+ >(queryEntitySubgraphQuery, {
+ variables: flowDefinitionsQueryVariables(
+ { all: [] },
+ currentTimeInstantTemporalAxes,
+ ),
+ fetchPolicy: "cache-and-network",
+ });
+
+ const flowDefinitions = useMemo(
+ () => getFlowDefinitionsFromQueryResult(data),
+ [data],
+ );
+
+ return { flowDefinitions, loading };
+};
+
+/**
+ * A flow definition as it was at a moment, e.g. when a run started: the edition the run executed.
+ */
+export const useFlowDefinitionAtTime = (params: {
+ flowDefinitionId: EntityUuid | null;
+ timestamp: Timestamp | null;
+}): { flowDefinition: FlowDefinitionWithId | null; loading: boolean } => {
+ const { flowDefinitionId, timestamp } = params;
+
+ const { data, loading } = useQuery<
+ QueryEntitySubgraphQuery,
+ QueryEntitySubgraphQueryVariables
+ >(queryEntitySubgraphQuery, {
+ variables: flowDefinitionsQueryVariables(
+ { equal: [{ path: ["uuid"] }, { parameter: flowDefinitionId }] },
+ timestamp
+ ? generateTimeInstantTemporalAxes(timestamp)
+ : currentTimeInstantTemporalAxes,
+ ),
+ skip: !flowDefinitionId || !timestamp,
+ fetchPolicy: "cache-first",
+ });
+
+ const flowDefinition = useMemo(
+ () => getFlowDefinitionsFromQueryResult(data)[0] ?? null,
+ [data],
+ );
+
+ return { flowDefinition, loading };
+};
diff --git a/apps/hash-frontend/src/pages/workers.page/flow-run-table.tsx b/apps/hash-frontend/src/pages/workers.page/flow-run-table.tsx
index 00902cba969..189fadac2fc 100644
--- a/apps/hash-frontend/src/pages/workers.page/flow-run-table.tsx
+++ b/apps/hash-frontend/src/pages/workers.page/flow-run-table.tsx
@@ -14,8 +14,8 @@ import {
InfinityLightIcon,
Skeleton,
} from "@hashintel/design-system";
+import { isGoalFlowDefinitionId } from "@local/hash-isomorphic-utils/flows/flow-definition-ids";
import { generateWorkerRunPath } from "@local/hash-isomorphic-utils/flows/frontend-paths";
-import { isGoalFlowDefinitionId } from "@local/hash-isomorphic-utils/flows/goal-flow-definitions";
import { Link } from "../../shared/ui/link";
import { useAuthenticatedUser } from "../shared/auth-info-context";
diff --git a/apps/plugin-browser/src/pages/popup/popup-contents/action-center/history/automatically-triggered.tsx b/apps/plugin-browser/src/pages/popup/popup-contents/action-center/history/automatically-triggered.tsx
index 42e30c4334b..4ee5b937b23 100644
--- a/apps/plugin-browser/src/pages/popup/popup-contents/action-center/history/automatically-triggered.tsx
+++ b/apps/plugin-browser/src/pages/popup/popup-contents/action-center/history/automatically-triggered.tsx
@@ -1,7 +1,7 @@
import { TableCell, TableRow } from "@mui/material";
import { useMemo } from "react";
-import { automaticBrowserInferenceFlow } from "@local/hash-isomorphic-utils/flows/browser-plugin-flow-definitions";
+import { flowDefinitionIds } from "@local/hash-isomorphic-utils/flows/flow-definition-ids";
import { EventTable } from "./shared/event-table";
import { HistoryRow } from "./shared/history-row";
@@ -19,7 +19,7 @@ export const AutomaticallyTriggered = ({
unfilteredFlowRuns.filter(
(flow) =>
flow.flowDefinitionId ===
- automaticBrowserInferenceFlow.flowDefinitionId ||
+ flowDefinitionIds.automaticBrowserInference ||
flow.requestedPageUrl,
),
[unfilteredFlowRuns],
diff --git a/apps/plugin-browser/src/pages/popup/popup-contents/action-center/history/manually-triggered.tsx b/apps/plugin-browser/src/pages/popup/popup-contents/action-center/history/manually-triggered.tsx
index e20594f0789..386212e2462 100644
--- a/apps/plugin-browser/src/pages/popup/popup-contents/action-center/history/manually-triggered.tsx
+++ b/apps/plugin-browser/src/pages/popup/popup-contents/action-center/history/manually-triggered.tsx
@@ -1,7 +1,7 @@
import { TableCell, TableRow } from "@mui/material";
import { useMemo } from "react";
-import { manualBrowserInferenceFlow } from "@local/hash-isomorphic-utils/flows/browser-plugin-flow-definitions";
+import { flowDefinitionIds } from "@local/hash-isomorphic-utils/flows/flow-definition-ids";
import { EventTable } from "./shared/event-table";
import { HistoryRow } from "./shared/history-row";
@@ -18,7 +18,7 @@ export const ManuallyTriggered = ({
() =>
unfilteredFlowRuns.filter(
(flow) =>
- flow.flowDefinitionId === manualBrowserInferenceFlow.flowDefinitionId,
+ flow.flowDefinitionId === flowDefinitionIds.manualBrowserInference,
),
[unfilteredFlowRuns],
);
diff --git a/apps/plugin-browser/src/pages/popup/popup-contents/action-center/history/shared/table-label.tsx b/apps/plugin-browser/src/pages/popup/popup-contents/action-center/history/shared/table-label.tsx
index ba0588dc2f3..32e93187322 100644
--- a/apps/plugin-browser/src/pages/popup/popup-contents/action-center/history/shared/table-label.tsx
+++ b/apps/plugin-browser/src/pages/popup/popup-contents/action-center/history/shared/table-label.tsx
@@ -1,10 +1,7 @@
import { Link, Stack, Typography } from "@mui/material";
import { ArrowUpRightRegularIcon } from "@hashintel/design-system";
-import {
- automaticBrowserInferenceFlow,
- manualBrowserInferenceFlow,
-} from "@local/hash-isomorphic-utils/flows/browser-plugin-flow-definitions";
+import { flowDefinitionIds } from "@local/hash-isomorphic-utils/flows/flow-definition-ids";
import { generatedFilteredWorkersPath } from "@local/hash-isomorphic-utils/flows/frontend-paths";
export const TableLabel = ({ type }: { type: "manual" | "automatic" }) => {
@@ -17,8 +14,8 @@ export const TableLabel = ({ type }: { type: "manual" | "automatic" }) => {
href={`${FRONTEND_ORIGIN}${generatedFilteredWorkersPath({
flowDefinitionIds:
type === "manual"
- ? [manualBrowserInferenceFlow.flowDefinitionId]
- : [automaticBrowserInferenceFlow.flowDefinitionId],
+ ? [flowDefinitionIds.manualBrowserInference]
+ : [flowDefinitionIds.automaticBrowserInference],
})}`}
sx={{
alignItems: "center",
diff --git a/apps/plugin-browser/src/pages/popup/popup-contents/action-center/shared/use-flow-runs.ts b/apps/plugin-browser/src/pages/popup/popup-contents/action-center/shared/use-flow-runs.ts
index a7fce63eeb2..1c324f86404 100644
--- a/apps/plugin-browser/src/pages/popup/popup-contents/action-center/shared/use-flow-runs.ts
+++ b/apps/plugin-browser/src/pages/popup/popup-contents/action-center/shared/use-flow-runs.ts
@@ -1,10 +1,7 @@
import { useEffect, useMemo, useState } from "react";
import { extractWebIdFromEntityId } from "@blockprotocol/type-system";
-import {
- automaticBrowserInferenceFlow,
- manualBrowserInferenceFlow,
-} from "@local/hash-isomorphic-utils/flows/browser-plugin-flow-definitions";
+import { flowDefinitionIds } from "@local/hash-isomorphic-utils/flows/flow-definition-ids";
import { getMinimalFlowRunsQuery } from "../../../../../graphql/queries/flow.queries";
import { queryGraphQlApi } from "../../../../../shared/query-graphql-api";
@@ -93,9 +90,9 @@ const getFlowRuns = async ({
for (const flowRun of unfilteredFlowRuns) {
if (
flowRun.flowDefinitionId ===
- manualBrowserInferenceFlow.flowDefinitionId ||
+ flowDefinitionIds.manualBrowserInference ||
flowRun.flowDefinitionId ===
- automaticBrowserInferenceFlow.flowDefinitionId
+ flowDefinitionIds.automaticBrowserInference
) {
flowRunsOfInterest.push(mapFlowRunToMinimalFlowRun(flowRun));
}
diff --git a/apps/plugin-browser/src/scripts/background/infer-entities.ts b/apps/plugin-browser/src/scripts/background/infer-entities.ts
index c5cac333e1b..32eb36515b3 100644
--- a/apps/plugin-browser/src/scripts/background/infer-entities.ts
+++ b/apps/plugin-browser/src/scripts/background/infer-entities.ts
@@ -2,10 +2,7 @@ import { v4 as uuid } from "uuid";
import browser from "webextension-polyfill";
import { entityIdFromComponents } from "@blockprotocol/type-system";
-import {
- automaticBrowserInferenceFlow,
- manualBrowserInferenceFlow,
-} from "@local/hash-isomorphic-utils/flows/browser-plugin-flow-definitions";
+import { flowDefinitionIds } from "@local/hash-isomorphic-utils/flows/flow-definition-ids";
import { sleep } from "@local/hash-isomorphic-utils/sleep";
import { FlowRunStatus } from "../../graphql/api-types.gen";
@@ -371,10 +368,10 @@ export const inferEntities = async (
await sendInferEntitiesMessage(inferenceArgs);
- const flowDefinition =
+ const flowDefinitionId =
trigger === "automatic"
- ? automaticBrowserInferenceFlow
- : manualBrowserInferenceFlow;
+ ? flowDefinitionIds.automaticBrowserInference
+ : flowDefinitionIds.manualBrowserInference;
/**
* Optimistically add the run to local storage so that it appears in the history tab immediately.
@@ -383,7 +380,7 @@ export const inferEntities = async (
await setLocalPendingRuns((existingValue) => [
...(existingValue ?? []),
{
- flowDefinitionId: flowDefinition.flowDefinitionId,
+ flowDefinitionId,
flowRunEntityId: entityIdFromComponents(webId, requestUuid),
closedAt: null,
executedAt: new Date().toISOString(),
diff --git a/libs/@local/hash-backend-utils/src/flows/payload-storage.test.ts b/libs/@local/hash-backend-utils/src/flows/payload-storage.test.ts
index 943cb6a0961..26eb3be60b5 100644
--- a/libs/@local/hash-backend-utils/src/flows/payload-storage.test.ts
+++ b/libs/@local/hash-backend-utils/src/flows/payload-storage.test.ts
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
+import { generateStepRunId } from "@local/hash-isomorphic-utils/flows/step-run-id";
import {
appendCollectedValue,
getArrayPayloadItems,
@@ -127,9 +128,12 @@ describe("stored arrays", () => {
const { storageProvider, getDownloadCount } = createMemoryStorageProvider();
const branches = await Promise.all([
- storeEntities(storageProvider, "3.2~0", [entity(0)]),
- storeEntities(storageProvider, "3.2~1", [entity(1), entity(2)]),
- storeEntities(storageProvider, "3.2~2", [entity(3)]),
+ storeEntities(storageProvider, generateStepRunId("3.2", 0), [entity(0)]),
+ storeEntities(storageProvider, generateStepRunId("3.2", 1), [
+ entity(1),
+ entity(2),
+ ]),
+ storeEntities(storageProvider, generateStepRunId("3.2", 2), [entity(3)]),
]);
const collected = branches.reduce<
@@ -168,8 +172,11 @@ describe("stored arrays", () => {
const { storageProvider, getDownloadCount } = createMemoryStorageProvider();
const branches = await Promise.all([
- storeEntities(storageProvider, "3.2~0", [entity(0)]),
- storeEntities(storageProvider, "3.2~1", [entity(1), entity(2)]),
+ storeEntities(storageProvider, generateStepRunId("3.2", 0), [entity(0)]),
+ storeEntities(storageProvider, generateStepRunId("3.2", 1), [
+ entity(1),
+ entity(2),
+ ]),
]);
const collected = appendCollectedValue(
@@ -198,7 +205,11 @@ describe("stored arrays", () => {
it("refuses to mix stored and inline arrays when collecting", async () => {
const { storageProvider } = createMemoryStorageProvider();
- const stored = await storeEntities(storageProvider, "3.2~0", [entity(0)]);
+ const stored = await storeEntities(
+ storageProvider,
+ generateStepRunId("3.2", 0),
+ [entity(0)],
+ );
expect(() => appendCollectedValue([entity(1)], stored)).toThrow();
expect(() => appendCollectedValue(stored, [entity(1)])).toThrow();
diff --git a/libs/@local/hash-backend-utils/src/flows/process-flow-workflow.ts b/libs/@local/hash-backend-utils/src/flows/process-flow-workflow.ts
index 7dbf46204e0..727211411d6 100644
--- a/libs/@local/hash-backend-utils/src/flows/process-flow-workflow.ts
+++ b/libs/@local/hash-backend-utils/src/flows/process-flow-workflow.ts
@@ -7,6 +7,10 @@ import {
import { actionDefinitions } from "@local/hash-isomorphic-utils/flows/action-definitions";
import { getFlowType } from "@local/hash-isomorphic-utils/flows/get-flow-type";
+import {
+ generateStepRunId,
+ parseStepRunId,
+} from "@local/hash-isomorphic-utils/flows/step-run-id";
import {
assertNoStoredPayloadRefsInFlowInputs,
getArrayPayloadItems,
@@ -37,7 +41,7 @@ import type {
CreateFlowActivities,
ProxyFlowActivity,
} from "./action-types.js";
-import type { EntityUuid } from "@blockprotocol/type-system";
+import type { EntityUuid, Timestamp } from "@blockprotocol/type-system";
import type { FlowActionStepOutput } from "@local/hash-isomorphic-utils/flows/action-definitions";
import type {
BaseRunFlowWorkflowParams,
@@ -83,10 +87,10 @@ const getProducerRunStepId = ({
consumerStepId: string;
producerStepId: string;
}) => {
- const [_, branchIndex] = consumerStepId.split("~");
+ const { itemIndex } = parseStepRunId(consumerStepId);
- if (branchIndex !== undefined) {
- const producerInBranch = `${producerStepId}~${branchIndex}`;
+ if (itemIndex !== undefined) {
+ const producerInBranch = generateStepRunId(producerStepId, itemIndex);
if (
getAllStepsInFlow(flow).some(({ stepId }) => stepId === producerInBranch)
@@ -320,9 +324,11 @@ const processFlow = async <
} = params;
/*
- * The run executes the definition's edition current when it starts.
+ * The run executes the definition's edition current when it starts: at the start of its execution chain, which a
+ * retry, replay or reset keeps. The frontend draws a run with the edition current then too.
*/
const flowDefinitionResult = await getFlowDefinitionActivity({
+ atTime: workflowInfo().startTime.toISOString() as Timestamp,
flowDefinitionId,
userAuthentication,
});
@@ -616,14 +622,14 @@ const processFlow = async <
? initializeActionStep({
flowInputs,
stepDefinition,
- overrideStepId: `${stepDefinition.stepId}~${index}`,
+ overrideStepId: generateStepRunId(stepDefinition.stepId, index),
existingFlow: flow,
item,
})
: initializeForEachStep({
flowInputs,
stepDefinition,
- overrideStepId: `${stepDefinition.stepId}~${index}`,
+ overrideStepId: generateStepRunId(stepDefinition.stepId, index),
existingFlow: flow,
item,
}),
diff --git a/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/common-activities/get-flow-definition-activity.ts b/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/common-activities/get-flow-definition-activity.ts
index 52015fa23b4..613110b82ed 100644
--- a/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/common-activities/get-flow-definition-activity.ts
+++ b/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/common-activities/get-flow-definition-activity.ts
@@ -6,18 +6,21 @@ import type {
ActorEntityUuid,
EntityId,
EntityUuid,
+ Timestamp,
} from "@blockprotocol/type-system";
import type { GraphApi } from "@local/hash-graph-client";
import type { FlowDefinition } from "@local/hash-isomorphic-utils/flows/types";
export type GetFlowDefinitionActivityParams = {
+ /** When the run started: it executes the edition of the definition current then. */
+ atTime: Timestamp;
flowDefinitionId: EntityUuid;
userAuthentication: { actorId: ActorEntityUuid };
};
/**
- * Loads the definition a run executes from its `Flow Definition` entity, and validates it. Returns the entity's id
- * too, for the run to link to.
+ * Loads the definition a run executes from its `Flow Definition` entity, as it was at `atTime`, and validates it.
+ * Returns the entity's id too, for the run to link to.
*/
export const getFlowDefinitionActivity = async (
params: GetFlowDefinitionActivityParams & { graphApiClient: GraphApi },
@@ -29,9 +32,11 @@ export const getFlowDefinitionActivity = async (
}
| { status: "not-found" | "invalid"; errorMessage: string }
> => {
- const { flowDefinitionId, graphApiClient, userAuthentication } = params;
+ const { atTime, flowDefinitionId, graphApiClient, userAuthentication } =
+ params;
const entity = await getFlowDefinitionEntityById({
+ atTime,
flowDefinitionId,
graphApiClient,
userAuthentication,
diff --git a/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/get-step-definition-from-flow.ts b/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/get-step-definition-from-flow.ts
index 780b7ed6a71..e97faa298ef 100644
--- a/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/get-step-definition-from-flow.ts
+++ b/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/get-step-definition-from-flow.ts
@@ -1,3 +1,4 @@
+import { parseStepRunId } from "@local/hash-isomorphic-utils/flows/step-run-id";
import { getAllStepDefinitionsInFlowDefinition } from "@local/hash-isomorphic-utils/flows/util";
import type {
@@ -20,7 +21,7 @@ export const getStepDefinitionFromFlowDefinition = <
}): T extends ActionStep ? ActionStepDefinition : ForEachStepDefinition => {
const { step, flowDefinition } = params;
- const [stepIdWithoutIndex] = step.stepId.split("~");
+ const { stepId: stepIdWithoutIndex } = parseStepRunId(step.stepId);
const stepDefinition = getAllStepDefinitionsInFlowDefinition(
flowDefinition,
diff --git a/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/pass-outputs-to-unprocessed-steps.ts b/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/pass-outputs-to-unprocessed-steps.ts
index 7d7c486fe5b..516daaf4aa0 100644
--- a/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/pass-outputs-to-unprocessed-steps.ts
+++ b/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/pass-outputs-to-unprocessed-steps.ts
@@ -1,3 +1,4 @@
+import { parseStepRunId } from "@local/hash-isomorphic-utils/flows/step-run-id";
import { createStepShapeResolver } from "@local/hash-isomorphic-utils/flows/step-shapes";
import {
appendCollectedValue,
@@ -33,7 +34,8 @@ export const passOutputsToUnprocessedSteps = (params: {
}): Omit, "contents"> => {
const { flow, flowDefinition, stepId, processedStepIds, outputs } = params;
- const [currentStepIdWithoutIndex, currentStepIdIndex] = stepId.split("~");
+ const { stepId: currentStepIdWithoutIndex, itemIndex: currentItemIndex } =
+ parseStepRunId(stepId);
const unprocessedSteps = getAllStepsInFlow(flow).filter(
(step) =>
@@ -43,9 +45,14 @@ export const passOutputsToUnprocessedSteps = (params: {
);
for (const unprocessedStep of unprocessedSteps) {
- const [_, unprocessedStepIdIndex] = unprocessedStep.stepId.split("~");
+ const { itemIndex: unprocessedItemIndex } = parseStepRunId(
+ unprocessedStep.stepId,
+ );
- if (currentStepIdIndex && currentStepIdIndex !== unprocessedStepIdIndex) {
+ if (
+ currentItemIndex !== undefined &&
+ currentItemIndex !== unprocessedItemIndex
+ ) {
/**
* A step in a for-each branch only feeds steps in the same branch.
*/
@@ -144,7 +151,7 @@ export const passOutputsToUnprocessedSteps = (params: {
processedStep.branchValues = {
...processedStep.branchValues,
- [Number(currentStepIdIndex)]: collectedOutput.payload.value,
+ [Number(currentItemIndex)]: collectedOutput.payload.value,
};
// eslint-disable-next-line @typescript-eslint/no-use-before-define -- the two pass each other's outputs on
@@ -250,7 +257,7 @@ export const collectSkippedBranchStep = (params: {
}): Omit, "contents"> => {
const { flow, flowDefinition, stepId, processedStepIds } = params;
- const [stepIdWithoutIndex, branchIndex] = stepId.split("~");
+ const { stepId: stepIdWithoutIndex, itemIndex } = parseStepRunId(stepId);
const forEachStep = getAllStepsInFlow(flow).find(
(step): step is ForEachStep =>
@@ -268,7 +275,7 @@ export const collectSkippedBranchStep = (params: {
forEachStep.branchValues = {
...forEachStep.branchValues,
- [Number(branchIndex)]: null,
+ [Number(itemIndex)]: null,
};
return completeForEachStep({
diff --git a/libs/@local/hash-backend-utils/src/flows/shared/get-flow-definition-entity-by-id.ts b/libs/@local/hash-backend-utils/src/flows/shared/get-flow-definition-entity-by-id.ts
index 3c6209ebc67..c988061c631 100644
--- a/libs/@local/hash-backend-utils/src/flows/shared/get-flow-definition-entity-by-id.ts
+++ b/libs/@local/hash-backend-utils/src/flows/shared/get-flow-definition-entity-by-id.ts
@@ -1,21 +1,30 @@
import { type HashEntity, queryEntities } from "@local/hash-graph-sdk/entity";
-import { currentTimeInstantTemporalAxes } from "@local/hash-isomorphic-utils/graph-queries";
+import {
+ currentTimeInstantTemporalAxes,
+ generateTimeInstantTemporalAxes,
+} from "@local/hash-isomorphic-utils/graph-queries";
import { systemEntityTypes } from "@local/hash-isomorphic-utils/ontology-type-ids";
-import type { ActorEntityUuid, EntityUuid } from "@blockprotocol/type-system";
+import type {
+ ActorEntityUuid,
+ EntityUuid,
+ Timestamp,
+} from "@blockprotocol/type-system";
import type { GraphApi } from "@local/hash-graph-client";
import type { FlowDefinition as FlowDefinitionEntity } from "@local/hash-isomorphic-utils/system-types/flowdefinition";
/**
* The `Flow Definition` entity of a flow definition, whose uuid is the flow definition's id: e.g. for a run or
- * trigger to link to.
+ * trigger to link to. Its edition current at `atTime`, if given, and otherwise its current edition.
*/
export const getFlowDefinitionEntityById = async (params: {
+ atTime?: Timestamp;
flowDefinitionId: EntityUuid;
graphApiClient: GraphApi;
userAuthentication: { actorId: ActorEntityUuid };
}): Promise | null> => {
- const { flowDefinitionId, graphApiClient, userAuthentication } = params;
+ const { atTime, flowDefinitionId, graphApiClient, userAuthentication } =
+ params;
const {
entities: [flowDefinitionEntity],
@@ -36,7 +45,9 @@ export const getFlowDefinitionEntityById = async (params: {
},
],
},
- temporalAxes: currentTimeInstantTemporalAxes,
+ temporalAxes: atTime
+ ? generateTimeInstantTemporalAxes(atTime)
+ : currentTimeInstantTemporalAxes,
includeDrafts: false,
includePermissions: false,
},
diff --git a/libs/@local/hash-isomorphic-utils/src/flows/browser-plugin-flow-definitions.ts b/libs/@local/hash-isomorphic-utils/src/flows/definitions/browser-plugin-flow-definitions.ts
similarity index 91%
rename from libs/@local/hash-isomorphic-utils/src/flows/browser-plugin-flow-definitions.ts
rename to libs/@local/hash-isomorphic-utils/src/flows/definitions/browser-plugin-flow-definitions.ts
index 551661e5e8d..faf8e5e76ab 100644
--- a/libs/@local/hash-isomorphic-utils/src/flows/browser-plugin-flow-definitions.ts
+++ b/libs/@local/hash-isomorphic-utils/src/flows/definitions/browser-plugin-flow-definitions.ts
@@ -1,5 +1,6 @@
-import { typedActionDefinitions as actions } from "./action-definitions.js";
-import { defineFlow, flowInput } from "./define-flow.js";
+import { typedActionDefinitions as actions } from "../action-definitions.js";
+import { defineFlow, flowInput } from "../define-flow.js";
+import { flowDefinitionIds } from "../flow-definition-ids.js";
const persistedEntitiesOutputDescription =
"The entities created or updated by the flow run";
@@ -12,8 +13,7 @@ const failedEntityProposalsOutputDescription =
*/
export const manualBrowserInferenceFlow = defineFlow(
{
- /* manual-browser-inference */
- flowDefinitionId: "a7bc5e44-f1ce-5126-b5db-8eb4cf054116",
+ flowDefinitionId: flowDefinitionIds.manualBrowserInference,
name: "Analyze webpage",
description: "Find entities of the requested types in a web page",
inputs: {
@@ -70,8 +70,7 @@ export const manualBrowserInferenceFlow = defineFlow(
*/
export const automaticBrowserInferenceFlow = defineFlow(
{
- /* automatic-browser-inference */
- flowDefinitionId: "a4385fa8-a457-584e-b81e-f7e49f3d0b3c",
+ flowDefinitionId: flowDefinitionIds.automaticBrowserInference,
name: "Auto-analyze webpage",
description:
"Find entities in a web page according to the user's passive analysis settings",
diff --git a/libs/@local/hash-isomorphic-utils/src/flows/file-flow-definitions.ts b/libs/@local/hash-isomorphic-utils/src/flows/definitions/file-flow-definitions.ts
similarity index 81%
rename from libs/@local/hash-isomorphic-utils/src/flows/file-flow-definitions.ts
rename to libs/@local/hash-isomorphic-utils/src/flows/definitions/file-flow-definitions.ts
index 650c62ff27e..71563662849 100644
--- a/libs/@local/hash-isomorphic-utils/src/flows/file-flow-definitions.ts
+++ b/libs/@local/hash-isomorphic-utils/src/flows/definitions/file-flow-definitions.ts
@@ -1,5 +1,6 @@
-import { typedActionDefinitions as actions } from "./action-definitions.js";
-import { defineFlow, flowInput } from "./define-flow.js";
+import { typedActionDefinitions as actions } from "../action-definitions.js";
+import { defineFlow, flowInput } from "../define-flow.js";
+import { flowDefinitionIds } from "../flow-definition-ids.js";
/**
* Started when a file is uploaded (see `graphql/resolvers/knowledge/file/shared.ts` in `hash-api`), with the
@@ -7,8 +8,7 @@ import { defineFlow, flowInput } from "./define-flow.js";
*/
export const inferMetadataFromDocumentFlow = defineFlow(
{
- /* infer-metadata-from-document */
- flowDefinitionId: "936f6a29-a964-533b-ae1a-03bed7da6471",
+ flowDefinitionId: flowDefinitionIds.inferMetadataFromDocument,
name: "Infer metadata from document",
description:
"Infer metadata from a document, assign appropriate type to document, and create associated entities.",
diff --git a/libs/@local/hash-isomorphic-utils/src/flows/frontend-flow-definitions.ts b/libs/@local/hash-isomorphic-utils/src/flows/definitions/frontend-flow-definitions.ts
similarity index 91%
rename from libs/@local/hash-isomorphic-utils/src/flows/frontend-flow-definitions.ts
rename to libs/@local/hash-isomorphic-utils/src/flows/definitions/frontend-flow-definitions.ts
index 531af2ad3d3..1fea517b591 100644
--- a/libs/@local/hash-isomorphic-utils/src/flows/frontend-flow-definitions.ts
+++ b/libs/@local/hash-isomorphic-utils/src/flows/definitions/frontend-flow-definitions.ts
@@ -1,7 +1,8 @@
-import { typedActionDefinitions as actions } from "./action-definitions.js";
-import { defineFlow, flowInput, type StepOutputs } from "./define-flow.js";
+import { typedActionDefinitions as actions } from "../action-definitions.js";
+import { defineFlow, flowInput, type StepOutputs } from "../define-flow.js";
+import { flowDefinitionIds } from "../flow-definition-ids.js";
-import type { TypedActionDefinitions } from "./action-definitions.js";
+import type { TypedActionDefinitions } from "../action-definitions.js";
/**
* The outputs the frontend reads from both dashboard item flows.
@@ -38,8 +39,7 @@ const dashboardItemOutputs = ({
*/
export const configureDashboardItemFlow = defineFlow(
{
- /* configure-dashboard-item */
- flowDefinitionId: "bf7f36a2-221e-5e7d-9879-289c1f56c148",
+ flowDefinitionId: flowDefinitionIds.configureDashboardItem,
name: "Configure Dashboard Item",
description:
"Generate query, analyze data, and create chart configuration for a dashboard item",
@@ -84,8 +84,7 @@ export const configureDashboardItemFlow = defineFlow(
export const refineDashboardItemFlow = defineFlow(
{
- /* refine-dashboard-item */
- flowDefinitionId: "6216ef71-5b7b-5468-bf12-9a34e30604b5",
+ flowDefinitionId: flowDefinitionIds.refineDashboardItem,
name: "Refine Dashboard Item",
description:
"Plan and apply a scoped refinement to an existing dashboard item",
diff --git a/libs/@local/hash-isomorphic-utils/src/flows/ftse-investors-flow-definition.ts b/libs/@local/hash-isomorphic-utils/src/flows/definitions/ftse-investors-flow-definition.ts
similarity index 92%
rename from libs/@local/hash-isomorphic-utils/src/flows/ftse-investors-flow-definition.ts
rename to libs/@local/hash-isomorphic-utils/src/flows/definitions/ftse-investors-flow-definition.ts
index 1e53daee740..833c5eb32c0 100644
--- a/libs/@local/hash-isomorphic-utils/src/flows/ftse-investors-flow-definition.ts
+++ b/libs/@local/hash-isomorphic-utils/src/flows/definitions/ftse-investors-flow-definition.ts
@@ -1,5 +1,6 @@
-import { typedActionDefinitions as actions } from "./action-definitions.js";
-import { constant, defineFlow, flowInput } from "./define-flow.js";
+import { typedActionDefinitions as actions } from "../action-definitions.js";
+import { constant, defineFlow, flowInput } from "../define-flow.js";
+import { flowDefinitionIds } from "../flow-definition-ids.js";
/**
* Researches the FTSE350's constituents, then each constituent's investors in a for-each step.
@@ -9,8 +10,7 @@ import { constant, defineFlow, flowInput } from "./define-flow.js";
*/
export const ftseInvestorsFlow = defineFlow(
{
- /* ftse-350-investors */
- flowDefinitionId: "25f32cd2-6f6e-5641-92b7-d9a90e517e0f",
+ flowDefinitionId: flowDefinitionIds.ftseInvestors,
name: "FTSE350 investors",
description:
"Research the FTSE350 index, its constituents, and the top investors in the index",
diff --git a/libs/@local/hash-isomorphic-utils/src/flows/goal-flow-definitions.ts b/libs/@local/hash-isomorphic-utils/src/flows/definitions/goal-flow-definitions.ts
similarity index 87%
rename from libs/@local/hash-isomorphic-utils/src/flows/goal-flow-definitions.ts
rename to libs/@local/hash-isomorphic-utils/src/flows/definitions/goal-flow-definitions.ts
index 4a4bad6ff52..37b4f94d722 100644
--- a/libs/@local/hash-isomorphic-utils/src/flows/goal-flow-definitions.ts
+++ b/libs/@local/hash-isomorphic-utils/src/flows/definitions/goal-flow-definitions.ts
@@ -1,5 +1,6 @@
-import { typedActionDefinitions as actions } from "./action-definitions.js";
-import { constant, defineFlow, flowInput } from "./define-flow.js";
+import { typedActionDefinitions as actions } from "../action-definitions.js";
+import { constant, defineFlow, flowInput } from "../define-flow.js";
+import { flowDefinitionIds } from "../flow-definition-ids.js";
const goalFlowInputs = {
researchGuidance: flowInput("Text", { label: "Research guidance" }),
@@ -35,8 +36,7 @@ export type GoalFlowInputName = keyof typeof goalFlowInputs;
*/
export const goalFlow = defineFlow(
{
- /* research-goal */
- flowDefinitionId: "ea4ac27d-7fe2-5400-ac72-ab4b41de9f67",
+ flowDefinitionId: flowDefinitionIds.goal,
name: "Research goal",
description:
"Discover entities according to a research brief and save them to HASH, optionally writing a report and saving the entities to a Google Sheet",
@@ -87,9 +87,3 @@ export const goalFlow = defineFlow(
};
},
);
-
-/**
- * Whether a run or definition is of the goal flow.
- */
-export const isGoalFlowDefinitionId = (flowDefinitionId: string) =>
- flowDefinitionId === goalFlow.flowDefinitionId;
diff --git a/libs/@local/hash-isomorphic-utils/src/flows/integration-flow-definitions.ts b/libs/@local/hash-isomorphic-utils/src/flows/definitions/integration-flow-definitions.ts
similarity index 90%
rename from libs/@local/hash-isomorphic-utils/src/flows/integration-flow-definitions.ts
rename to libs/@local/hash-isomorphic-utils/src/flows/definitions/integration-flow-definitions.ts
index e09bd930767..4967d41510d 100644
--- a/libs/@local/hash-isomorphic-utils/src/flows/integration-flow-definitions.ts
+++ b/libs/@local/hash-isomorphic-utils/src/flows/definitions/integration-flow-definitions.ts
@@ -1,13 +1,13 @@
-import { typedActionDefinitions as actions } from "./action-definitions.js";
-import { defineFlow, flowInput } from "./define-flow.js";
+import { typedActionDefinitions as actions } from "../action-definitions.js";
+import { defineFlow, flowInput } from "../define-flow.js";
+import { flowDefinitionIds } from "../flow-definition-ids.js";
/**
* Flow for fetching historical flight arrivals for an airport over a date range and persisting them to the graph.
*/
export const historicalFlightsFlow = defineFlow(
{
- /* historical-flights */
- flowDefinitionId: "113ce2ec-6a12-56b6-b495-072475889b37",
+ flowDefinitionId: flowDefinitionIds.historicalFlights,
name: "Get Historical Flights",
description:
"Fetch and save historical flight arrivals for an airport over a date range.",
@@ -47,8 +47,7 @@ export const historicalFlightsFlow = defineFlow(
*/
export const scheduledFlightsFlow = defineFlow(
{
- /* scheduled-flights */
- flowDefinitionId: "96a80402-c41b-5978-a018-b30f7e09bfe0",
+ flowDefinitionId: flowDefinitionIds.scheduledFlights,
name: "Get Scheduled Flights",
description:
"Fetch and save scheduled flight arrivals for an airport on a given date, with position updates for live flights.",
diff --git a/libs/@local/hash-isomorphic-utils/src/flows/flow-definition-ids.ts b/libs/@local/hash-isomorphic-utils/src/flows/flow-definition-ids.ts
new file mode 100644
index 00000000000..6a12288f7db
--- /dev/null
+++ b/libs/@local/hash-isomorphic-utils/src/flows/flow-definition-ids.ts
@@ -0,0 +1,28 @@
+import type { EntityUuid } from "@blockprotocol/type-system";
+
+/**
+ * The ids of the flows defined in code, which are also the entity uuids of their `Flow Definition` entities.
+ *
+ * Code that starts a run of one of these flows, or recognises its runs, uses its id from here. The definitions are
+ * in `definitions/`, which only seeding, the compiler and tests use: runs execute the definition stored in the
+ * graph, and the frontend draws it from there.
+ */
+export const flowDefinitionIds = {
+ goal: "ea4ac27d-7fe2-5400-ac72-ab4b41de9f67" as EntityUuid,
+ manualBrowserInference: "a7bc5e44-f1ce-5126-b5db-8eb4cf054116" as EntityUuid,
+ automaticBrowserInference:
+ "a4385fa8-a457-584e-b81e-f7e49f3d0b3c" as EntityUuid,
+ inferMetadataFromDocument:
+ "936f6a29-a964-533b-ae1a-03bed7da6471" as EntityUuid,
+ configureDashboardItem: "bf7f36a2-221e-5e7d-9879-289c1f56c148" as EntityUuid,
+ refineDashboardItem: "6216ef71-5b7b-5468-bf12-9a34e30604b5" as EntityUuid,
+ historicalFlights: "113ce2ec-6a12-56b6-b495-072475889b37" as EntityUuid,
+ scheduledFlights: "96a80402-c41b-5978-a018-b30f7e09bfe0" as EntityUuid,
+ ftseInvestors: "25f32cd2-6f6e-5641-92b7-d9a90e517e0f" as EntityUuid,
+} as const;
+
+/**
+ * Whether a run or definition is of the goal flow.
+ */
+export const isGoalFlowDefinitionId = (flowDefinitionId: string) =>
+ flowDefinitionId === flowDefinitionIds.goal;
diff --git a/libs/@local/hash-isomorphic-utils/src/flows/flow-definition-schema.ts b/libs/@local/hash-isomorphic-utils/src/flows/flow-definition-schema.ts
index 8b138bfe777..009ddf51322 100644
--- a/libs/@local/hash-isomorphic-utils/src/flows/flow-definition-schema.ts
+++ b/libs/@local/hash-isomorphic-utils/src/flows/flow-definition-schema.ts
@@ -1,5 +1,6 @@
import { z } from "zod";
+import { stepRunIdSeparator } from "./step-run-id.js";
import { allPayloadKinds } from "./types.js";
import type {
@@ -23,8 +24,8 @@ const connectionOptionsShape = {
const stepIdSchema = z
.string()
.min(1)
- .refine((stepId) => !stepId.includes("~"), {
- message: 'A step id cannot contain "~"',
+ .refine((stepId) => !stepId.includes(stepRunIdSeparator), {
+ message: `A step id cannot contain "${stepRunIdSeparator}"`,
});
const stepInputSourceSchema = z.discriminatedUnion("kind", [
diff --git a/libs/@local/hash-isomorphic-utils/src/flows/step-run-id.ts b/libs/@local/hash-isomorphic-utils/src/flows/step-run-id.ts
new file mode 100644
index 00000000000..0d315c6f514
--- /dev/null
+++ b/libs/@local/hash-isomorphic-utils/src/flows/step-run-id.ts
@@ -0,0 +1,34 @@
+/**
+ * Separates a step's id from the index of the `for-each` item a run of the step is for. Step ids can't contain it.
+ */
+export const stepRunIdSeparator = "~";
+
+/**
+ * The id of the run of a step inside a `for-each` for one of its items: the step's id and the item's index.
+ */
+export type ForEachItemStepRunId =
+ `${string}${typeof stepRunIdSeparator}${number}`;
+
+export const generateStepRunId = (
+ stepId: string,
+ itemIndex: number,
+): ForEachItemStepRunId => `${stepId}${stepRunIdSeparator}${itemIndex}`;
+
+/**
+ * The step a run is of, and the index of the `for-each` item it's for, if any. A run's id is its step's own id, or for
+ * a step inside a `for-each`, a {@link ForEachItemStepRunId} per item.
+ */
+export const parseStepRunId = (
+ stepRunId: string,
+): { stepId: string; itemIndex?: number } => {
+ const separatorIndex = stepRunId.indexOf(stepRunIdSeparator);
+
+ if (separatorIndex === -1) {
+ return { stepId: stepRunId };
+ }
+
+ return {
+ stepId: stepRunId.slice(0, separatorIndex),
+ itemIndex: Number(stepRunId.slice(separatorIndex + 1)),
+ };
+};
diff --git a/libs/@local/hash-isomorphic-utils/src/flows/validate-flow-definition.test.ts b/libs/@local/hash-isomorphic-utils/src/flows/validate-flow-definition.test.ts
index cb4e98d0a54..568a7d5360a 100644
--- a/libs/@local/hash-isomorphic-utils/src/flows/validate-flow-definition.test.ts
+++ b/libs/@local/hash-isomorphic-utils/src/flows/validate-flow-definition.test.ts
@@ -1,7 +1,7 @@
import { describe, expect, it } from "vitest";
-import { ftseInvestorsFlow } from "./ftse-investors-flow-definition.js";
-import { goalFlow } from "./goal-flow-definitions.js";
+import { ftseInvestorsFlow } from "./definitions/ftse-investors-flow-definition.js";
+import { goalFlow } from "./definitions/goal-flow-definitions.js";
import {
type FlowDefinitionDiagnostic,
validateFlowDefinition,