From 51072eec4bd82dfea9f538f6cce91fcdd5b987e8 Mon Sep 17 00:00:00 2001 From: Ciaran Morinan <37743469+CiaranMn@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:35:47 +0100 Subject: [PATCH 01/13] Identify flow runs by their entity id rather than their uuid, and find a run's workflow from its entity --- .../src/ai/infer-entities-websocket.ts | 15 +++---- .../handle-cancel-infer-entities-request.ts | 9 +++-- .../graphql/resolvers/flows/cancel-flow.ts | 12 +++--- .../resolvers/flows/get-flow-run-by-id.ts | 7 ++-- .../src/graphql/resolvers/flows/reset-flow.ts | 12 +++--- .../src/graphql/resolvers/flows/start-flow.ts | 9 +++-- .../flows/submit-external-input-response.ts | 12 +++--- .../graphql/queries/knowledge/flow.queries.ts | 12 ++++-- .../[shortname]/flows/[flow-def-id].page.tsx | 2 +- .../@/[shortname]/shared/flow-visualizer.tsx | 10 ++--- .../shared/flow-visualizer/activity-log.tsx | 2 +- .../flow-visualizer/flow-run-sidebar.tsx | 10 ++--- .../flow-visualizer/outputs/claims-output.tsx | 8 +--- .../flow-visualizer/shared/question-modal.tsx | 6 +-- .../shared/flow-visualizer/topbar.tsx | 22 ++++++----- .../@/[shortname]/workers/[run-id].page.tsx | 14 ++++++- .../pages/dashboard/[dashboard-id].page.tsx | 2 +- .../[dashboard-id].page/item-config-modal.tsx | 2 +- .../hooks/use-dashboard-item-config.ts | 24 ++++++------ .../hooks/use-dashboard-item-generations.ts | 4 +- apps/hash-frontend/src/pages/flows.page.tsx | 2 +- apps/hash-frontend/src/pages/goals.page.tsx | 2 +- .../src/pages/goals.page/goals-list.tsx | 4 +- .../goals-list/goal-list-section.tsx | 2 +- .../goals-list/goal-list-section/goal-row.tsx | 11 ++++-- .../src/pages/goals/new.page.tsx | 6 +-- .../history-table/provenance.tsx | 3 +- .../shared/flow-runs-context-provider.tsx | 17 ++++---- .../src/pages/shared/flow-runs-context.ts | 3 +- .../src/pages/shared/use-flow-runs-usage.ts | 39 +++++++++---------- apps/hash-frontend/src/pages/workers.page.tsx | 5 ++- .../src/pages/workers.page/flow-run-table.tsx | 19 ++++----- .../src/graphql/queries/flow.queries.ts | 2 +- .../history/automatically-triggered.tsx | 4 +- .../history/manually-triggered.tsx | 6 ++- .../history/shared/history-row.tsx | 2 +- .../flow-metadata-cell-contents.tsx | 20 +++++++--- .../shared/history-row/flow-status-cell.tsx | 2 +- .../action-center/shared/use-flow-runs.ts | 8 ++-- .../src/scripts/background/infer-entities.ts | 15 +++---- apps/plugin-browser/src/shared/messages.ts | 4 +- apps/plugin-browser/src/shared/storage.ts | 2 +- libs/@local/hash-backend-utils/src/flows.ts | 38 +++++++----------- .../src/flows/get-flow-run-details.ts | 14 +++---- .../src/flows/process-flow-workflow.ts | 4 +- .../persist-flow-activity.ts | 10 ++--- .../flows/shared/get-flow-run-entity-by-id.ts | 17 +++++--- .../src/ai-inference-types.ts | 12 ++++-- .../src/create-apollo-client.ts | 2 +- .../src/flows/frontend-paths.ts | 15 +++++-- .../src/flows/temporal-types.ts | 2 +- .../src/graphql/queries/flow.queries.ts | 15 ++++--- .../type-defs/knowledge/flow.typedef.ts | 21 +++++----- 53 files changed, 284 insertions(+), 238 deletions(-) diff --git a/apps/hash-api/src/ai/infer-entities-websocket.ts b/apps/hash-api/src/ai/infer-entities-websocket.ts index 688469ad700..c3003e2a896 100644 --- a/apps/hash-api/src/ai/infer-entities-websocket.ts +++ b/apps/hash-api/src/ai/infer-entities-websocket.ts @@ -13,7 +13,6 @@ import { handleInferEntitiesRequest } from "./infer-entities-websocket/handle-in import type { GraphApi, ImpureGraphContext } from "../graph/context-types"; import type { User } from "../graph/knowledge/system-types/user"; -import type { EntityUuid } from "@blockprotocol/type-system"; import type { FileStorageProvider } from "@local/hash-backend-utils/file-storage"; import type { Logger } from "@local/hash-backend-utils/logger"; import type { @@ -79,7 +78,7 @@ const inferEntitiesMessageHandler = async ({ for (const inputRequest of flowRun.inputRequests) { if (!inputRequest.resolvedAt) { const requestMessage: ExternalInputWebsocketRequestMessage = { - workflowId: flowRun.flowRunId, + flowRunEntityId: flowRun.flowRunEntityId, payload: inputRequest, type: "external-input-request", }; @@ -90,11 +89,11 @@ const inferEntitiesMessageHandler = async ({ return; } case "external-input-response": { - const { workflowId, payload } = message; + const { flowRunEntityId, payload } = message; const flow = await getFlowRunEntityById({ userAuthentication: { actorId: user.accountId }, - flowRunId: workflowId as EntityUuid, + flowRunEntityId, graphApiClient, }); @@ -102,7 +101,9 @@ const inferEntitiesMessageHandler = async ({ return; } - const handle = temporalClient.workflow.getHandle(workflowId); + const handle = temporalClient.workflow.getHandle( + flow.properties["https://hash.ai/@h/types/property-type/workflow-id/"], + ); await handle.signal<[ExternalInputResponseSignal]>( externalInputResponseSignal, { ...payload, resolvedBy: user.accountId }, @@ -142,7 +143,7 @@ const isInferenceWebsocketClientMessage = ( ); case "cancel-inference-request": return ( - typeof message.flowRunId === "string" && + typeof message.flowRunEntityId === "string" && typeof message.requestUuid === "string" && hasValidOptionalCookie(message) ); @@ -150,7 +151,7 @@ const isInferenceWebsocketClientMessage = ( return hasValidOptionalCookie(message); case "external-input-response": return ( - typeof message.workflowId === "string" && + typeof message.flowRunEntityId === "string" && typeof message.payload === "object" && message.payload !== null && hasValidOptionalCookie(message) diff --git a/apps/hash-api/src/ai/infer-entities-websocket/handle-cancel-infer-entities-request.ts b/apps/hash-api/src/ai/infer-entities-websocket/handle-cancel-infer-entities-request.ts index 45737e15632..6b389e84a64 100644 --- a/apps/hash-api/src/ai/infer-entities-websocket/handle-cancel-infer-entities-request.ts +++ b/apps/hash-api/src/ai/infer-entities-websocket/handle-cancel-infer-entities-request.ts @@ -2,7 +2,6 @@ import { getFlowRunEntityById } from "@local/hash-backend-utils/flows"; import type { GraphApi } from "../../graph/context-types"; import type { User } from "../../graph/knowledge/system-types/user"; -import type { EntityUuid } from "@blockprotocol/type-system"; import type { CancelInferEntitiesWebsocketRequestMessage } from "@local/hash-isomorphic-utils/ai-inference-types"; import type { Client } from "@temporalio/client"; @@ -17,11 +16,11 @@ export const handleCancelInferEntitiesRequest = async ({ message: Omit; user: User; }) => { - const { flowRunId } = message; + const { flowRunEntityId } = message; const flow = await getFlowRunEntityById({ userAuthentication: { actorId: user.accountId }, - flowRunId: flowRunId as EntityUuid, + flowRunEntityId, graphApiClient, }); @@ -29,7 +28,9 @@ export const handleCancelInferEntitiesRequest = async ({ return; } - const workflowHandle = temporalClient.workflow.getHandle(flowRunId); + const workflowHandle = temporalClient.workflow.getHandle( + flow.properties["https://hash.ai/@h/types/property-type/workflow-id/"], + ); void workflowHandle.cancel(); }; diff --git a/apps/hash-api/src/graphql/resolvers/flows/cancel-flow.ts b/apps/hash-api/src/graphql/resolvers/flows/cancel-flow.ts index c1792600628..e76e02d96e7 100644 --- a/apps/hash-api/src/graphql/resolvers/flows/cancel-flow.ts +++ b/apps/hash-api/src/graphql/resolvers/flows/cancel-flow.ts @@ -7,7 +7,6 @@ import * as Error from "../../error"; import type { MutationResetFlowArgs, ResolverFn } from "../../api-types.gen"; import type { LoggedInGraphQLContext } from "../../context"; -import type { EntityUuid } from "@blockprotocol/type-system"; export const cancelFlow: ResolverFn< Promise, @@ -16,19 +15,22 @@ export const cancelFlow: ResolverFn< MutationResetFlowArgs > = async ( _, - { flowUuid }, + { flowRunEntityId }, { authentication, dataSources, provenance, temporal, user }, ) => { const flow = await getFlowRunEntityById({ - flowRunId: flowUuid as EntityUuid, + flowRunEntityId, graphApiClient: dataSources.graphApi, userAuthentication: authentication, }); if (!flow) { - throw Error.notFound(`Flow with id ${flowUuid} not found`); + throw Error.notFound(`Flow with id ${flowRunEntityId} not found`); } + const workflowId = + flow.properties["https://hash.ai/@h/types/property-type/workflow-id/"]; + const userCanModify = await checkEntityPermission( { graphApi: dataSources.graphApi, provenance }, authentication, @@ -45,7 +47,7 @@ export const cancelFlow: ResolverFn< await temporal.workflowService.requestCancelWorkflowExecution({ namespace: temporalNamespace, workflowExecution: { - workflowId: flowUuid, + workflowId, }, reason: `Cancelled via GraphQL API by @${user.shortname}`, requestId: generateUuid(), diff --git a/apps/hash-api/src/graphql/resolvers/flows/get-flow-run-by-id.ts b/apps/hash-api/src/graphql/resolvers/flows/get-flow-run-by-id.ts index 0e647e13cbd..ff209e131b0 100644 --- a/apps/hash-api/src/graphql/resolvers/flows/get-flow-run-by-id.ts +++ b/apps/hash-api/src/graphql/resolvers/flows/get-flow-run-by-id.ts @@ -9,7 +9,6 @@ import type { ResolverFn, } from "../../api-types.gen"; import type { GraphQLContext } from "../../context"; -import type { EntityUuid } from "@blockprotocol/type-system"; import type { SparseFlowRun } from "@local/hash-isomorphic-utils/flows/types"; export const getFlowRunByIdResolver: ResolverFn< @@ -18,12 +17,12 @@ export const getFlowRunByIdResolver: ResolverFn< Pick, QueryGetFlowRunByIdArgs > = async (_parent, args, context, info) => { - const { flowRunId } = args; + const { flowRunEntityId } = args; const includeDetails = wereDetailedFieldsRequested(info); const flowRun = await getFlowRunById({ - flowRunId: flowRunId as EntityUuid, + flowRunEntityId, graphApiClient: context.dataSources.graphApi, includeDetails, storageProvider: context.dataSources.uploadProvider, @@ -32,7 +31,7 @@ export const getFlowRunByIdResolver: ResolverFn< }); if (!flowRun) { - throw Error.notFound(`Flow run with id ${flowRunId} not found`); + throw Error.notFound(`Flow run with id ${flowRunEntityId} not found`); } return flowRun; diff --git a/apps/hash-api/src/graphql/resolvers/flows/reset-flow.ts b/apps/hash-api/src/graphql/resolvers/flows/reset-flow.ts index f5811ba8003..02b4495bf19 100644 --- a/apps/hash-api/src/graphql/resolvers/flows/reset-flow.ts +++ b/apps/hash-api/src/graphql/resolvers/flows/reset-flow.ts @@ -10,7 +10,6 @@ import * as Error from "../../error"; import type { MutationResetFlowArgs, ResolverFn } from "../../api-types.gen"; import type { LoggedInGraphQLContext } from "../../context"; -import type { EntityUuid } from "@blockprotocol/type-system"; export const resetFlow: ResolverFn< Promise, @@ -19,19 +18,22 @@ export const resetFlow: ResolverFn< MutationResetFlowArgs > = async ( _, - { flowUuid, checkpointId, eventId }, + { flowRunEntityId, checkpointId, eventId }, { authentication, dataSources, provenance, temporal }, ) => { const flow = await getFlowRunEntityById({ - flowRunId: flowUuid as EntityUuid, + flowRunEntityId, graphApiClient: dataSources.graphApi, userAuthentication: authentication, }); if (!flow) { - throw Error.notFound(`Flow with id ${flowUuid} not found`); + throw Error.notFound(`Flow with id ${flowRunEntityId} not found`); } + const workflowId = + flow.properties["https://hash.ai/@h/types/property-type/workflow-id/"]; + const userCanModify = await checkEntityPermission( { graphApi: dataSources.graphApi, provenance }, authentication, @@ -48,7 +50,7 @@ export const resetFlow: ResolverFn< await temporal.workflowService.resetWorkflowExecution({ namespace: temporalNamespace, workflowExecution: { - workflowId: flowUuid, + workflowId, }, /** * If we don't set this, Temporal will replay signals sent after the cancellation point to the new workflow execution. diff --git a/apps/hash-api/src/graphql/resolvers/flows/start-flow.ts b/apps/hash-api/src/graphql/resolvers/flows/start-flow.ts index 123f17a8509..f35d82ec817 100644 --- a/apps/hash-api/src/graphql/resolvers/flows/start-flow.ts +++ b/apps/hash-api/src/graphql/resolvers/flows/start-flow.ts @@ -1,3 +1,4 @@ +import { entityIdFromComponents } from "@blockprotocol/type-system"; import { generateUuid } from "@local/hash-isomorphic-utils/generate-uuid"; import { @@ -8,14 +9,14 @@ import * as Error from "../../error"; import { getRunnableFlowDefinition } from "./shared/get-runnable-flow-definition"; import type { LoggedInGraphQLContext } from "../../context"; -import type { EntityUuid } from "@blockprotocol/type-system"; +import type { EntityId, EntityUuid } from "@blockprotocol/type-system"; import type { RunFlowWorkflowParams, RunFlowWorkflowResponse, } from "@local/hash-isomorphic-utils/flows/temporal-types"; export const startFlow: ResolverFn< - Promise, + Promise, Record, LoggedInGraphQLContext, MutationStartFlowArgs @@ -42,7 +43,7 @@ export const startFlow: ResolverFn< const params: RunFlowWorkflowParams = { ...(flowType === "ai" ? { dataSources } : {}), - flowRunId: workflowId, + flowRunEntityUuid: workflowId, flowInputs, flowDefinitionId, userAuthentication: { actorId: user.accountId }, @@ -65,5 +66,5 @@ export const startFlow: ResolverFn< }, }); - return workflowId; + return entityIdFromComponents(webId, workflowId); }; diff --git a/apps/hash-api/src/graphql/resolvers/flows/submit-external-input-response.ts b/apps/hash-api/src/graphql/resolvers/flows/submit-external-input-response.ts index 91ddb0ed5f6..d2ecbfe5afd 100644 --- a/apps/hash-api/src/graphql/resolvers/flows/submit-external-input-response.ts +++ b/apps/hash-api/src/graphql/resolvers/flows/submit-external-input-response.ts @@ -8,7 +8,6 @@ import type { ResolverFn, } from "../../api-types.gen"; import type { LoggedInGraphQLContext } from "../../context"; -import type { EntityUuid } from "@blockprotocol/type-system"; export const submitExternalInputResponse: ResolverFn< Promise, @@ -17,20 +16,23 @@ export const submitExternalInputResponse: ResolverFn< MutationSubmitExternalInputResponseArgs > = async ( _, - { response, flowUuid }, + { response, flowRunEntityId }, { authentication, dataSources, temporal }, ) => { const flow = await getFlowRunEntityById({ - flowRunId: flowUuid as EntityUuid, + flowRunEntityId, graphApiClient: dataSources.graphApi, userAuthentication: authentication, }); if (!flow) { - throw Error.notFound(`Flow with id ${flowUuid} not found`); + throw Error.notFound(`Flow with id ${flowRunEntityId} not found`); } - const handle = temporal.workflow.getHandle(flowUuid); + const workflowId = + flow.properties["https://hash.ai/@h/types/property-type/workflow-id/"]; + + const handle = temporal.workflow.getHandle(workflowId); await handle.signal(externalInputResponseSignal, response); diff --git a/apps/hash-frontend/src/graphql/queries/knowledge/flow.queries.ts b/apps/hash-frontend/src/graphql/queries/knowledge/flow.queries.ts index 772aa942acb..ed1c80643b9 100644 --- a/apps/hash-frontend/src/graphql/queries/knowledge/flow.queries.ts +++ b/apps/hash-frontend/src/graphql/queries/knowledge/flow.queries.ts @@ -17,18 +17,22 @@ export const startFlowMutation = gql` `; export const resetFlowMutation = gql` - mutation resetFlow($flowUuid: ID!, $checkpointId: ID!, $eventId: Int!) { + mutation resetFlow( + $flowRunEntityId: EntityId! + $checkpointId: ID! + $eventId: Int! + ) { resetFlow( checkpointId: $checkpointId eventId: $eventId - flowUuid: $flowUuid + flowRunEntityId: $flowRunEntityId ) } `; export const cancelFlowMutation = gql` - mutation cancelFlow($flowUuid: ID!) { - cancelFlow(flowUuid: $flowUuid) + mutation cancelFlow($flowRunEntityId: EntityId!) { + cancelFlow(flowRunEntityId: $flowRunEntityId) } `; diff --git a/apps/hash-frontend/src/pages/@/[shortname]/flows/[flow-def-id].page.tsx b/apps/hash-frontend/src/pages/@/[shortname]/flows/[flow-def-id].page.tsx index 51cd401c862..ecafb83d3f8 100644 --- a/apps/hash-frontend/src/pages/@/[shortname]/flows/[flow-def-id].page.tsx +++ b/apps/hash-frontend/src/pages/@/[shortname]/flows/[flow-def-id].page.tsx @@ -18,7 +18,7 @@ const FlowDefinitionPage: NextPageWithLayout = () => { - + 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 e7558c3831b..7e313a1d427 100644 --- a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer.tsx +++ b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer.tsx @@ -603,8 +603,8 @@ export const FlowVisualizer = () => { try { const { data } = await startFlow({ variables }); - const flowRunId = data?.startFlow; - if (!flowRunId) { + const flowRunEntityId = data?.startFlow; + if (!flowRunEntityId) { throw new Error("Failed to start flow"); } @@ -616,7 +616,7 @@ export const FlowVisualizer = () => { const { shortname } = getOwner({ webId: variables.webId }); - void push(generateWorkerRunPath({ shortname, flowRunId })); + void push(generateWorkerRunPath({ shortname, flowRunEntityId })); } finally { setStartFlowPending(false); } @@ -646,7 +646,7 @@ export const FlowVisualizer = () => { const flowDefinitionStateKey = `${selectedFlowDefinition.name} ?? "loading`; const flowRunStateKey = `${flowDefinitionStateKey}-${ - selectedFlowRun?.flowRunId ?? "definition" + selectedFlowRun?.flowRunEntityId ?? "definition" }`; const isRunnableFromHere = !unrunnableDefinitionIds.includes( @@ -707,7 +707,7 @@ export const FlowVisualizer = () => { {selectedFlowRun ? ( { void resetFlow({ variables: { - flowUuid: selectedFlowRun.flowRunId, + flowRunEntityId: selectedFlowRun.flowRunEntityId, checkpointId: log.checkpointId, eventId: log.eventId, }, 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 45c8d74aefc..ef523fbe993 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 @@ -22,7 +22,7 @@ import { flowSectionBorderRadius } from "./shared/styles"; import type { FlowRun } from "../../../../../graphql/api-types.gen"; import type { EdgesAndNodes } from "./shared/types"; -import type { EntityUuid } from "@blockprotocol/type-system"; +import type { EntityId, EntityUuid } from "@blockprotocol/type-system"; import type { PropsWithChildren } from "react"; const SidebarSection = ({ children }: PropsWithChildren) => ( @@ -41,7 +41,7 @@ const SidebarSection = ({ children }: PropsWithChildren) => ( type FlowRunSidebarProps = { flowDefinitionId: EntityUuid; - flowRunId: EntityUuid; + flowRunEntityId: EntityId; flowScheduleId: EntityUuid | null; graph: EdgesAndNodes; name: FlowRun["name"]; @@ -50,7 +50,7 @@ type FlowRunSidebarProps = { export const FlowRunSidebar = ({ flowDefinitionId, - flowRunId, + flowRunEntityId, flowScheduleId, graph, name, @@ -59,14 +59,14 @@ export const FlowRunSidebar = ({ const { selectedFlowRun } = useFlowRunsContext(); const { isUsageAvailable, usageByFlowRun } = useFlowRunsUsage({ - flowRunIds: [flowRunId], + flowRunEntityIds: [flowRunEntityId], pollInterval: selectedFlowRun?.closedAt ? 0 : 5_000, }); const [showUsageBreakdown, setShowUsageBreakdown] = useState(false); const [showResearchPrompt, setShowResearchPrompt] = useState(false); - const usage = usageByFlowRun[flowRunId]; + const usage = usageByFlowRun[flowRunEntityId]; const nameParts = useMemo<{ text: string; url?: boolean }[]>(() => { const parts = name.split(/( )/g); diff --git a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/outputs/claims-output.tsx b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/outputs/claims-output.tsx index 11faa671a0a..5d087c1d419 100644 --- a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/outputs/claims-output.tsx +++ b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/outputs/claims-output.tsx @@ -2,7 +2,6 @@ import { useQuery } from "@apollo/client"; import { memo, useLayoutEffect, useMemo, useRef, useState } from "react"; import { getRoots } from "@blockprotocol/graph/stdlib"; -import { entityIdFromComponents } from "@blockprotocol/type-system"; import { deserializeQueryEntitySubgraphResponse } from "@local/hash-graph-sdk/entity"; import { currentTimeInstantTemporalAxes } from "@local/hash-isomorphic-utils/graph-queries"; import { systemEntityTypes } from "@local/hash-isomorphic-utils/ontology-type-ids"; @@ -51,12 +50,7 @@ export const ClaimsOutput = memo(({ proposedEntities }: ClaimsTableProps) => { path: ["editionProvenance", "origin", "id"], }, { - parameter: selectedFlowRun - ? entityIdFromComponents( - selectedFlowRun.webId, - selectedFlowRun.flowRunId, - ) - : "never", + parameter: selectedFlowRun?.flowRunEntityId ?? "never", }, ], }, diff --git a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/shared/question-modal.tsx b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/shared/question-modal.tsx index bc82f4c67b3..92b77ddfefb 100644 --- a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/shared/question-modal.tsx +++ b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/shared/question-modal.tsx @@ -69,7 +69,7 @@ export const QuestionModal = ({ ); } - const { selectedFlowRunId } = useFlowRunsContext(); + const { selectedFlowRunEntityId } = useFlowRunsContext(); const [submitExternalInputResponse] = useMutation< SubmitExternalInputResponseMutation, @@ -87,13 +87,13 @@ export const QuestionModal = ({ ); const submitAnswers = async () => { - if (!allQuestionsAnswered || !selectedFlowRunId) { + if (!allQuestionsAnswered || !selectedFlowRunEntityId) { return; } await submitExternalInputResponse({ variables: { - flowUuid: selectedFlowRunId, + flowRunEntityId: selectedFlowRunEntityId, response: { requestId: inputRequest.requestId, type: "human-input", diff --git a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/topbar.tsx b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/topbar.tsx index bf1ad6bf4da..8f2dff42218 100644 --- a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/topbar.tsx +++ b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/topbar.tsx @@ -35,7 +35,6 @@ import type { CancelFlowMutationVariables, FlowRun, } from "../../../../../graphql/api-types.gen"; -import type { EntityUuid } from "@blockprotocol/type-system"; import type { SvgIconProps, SxProps, Theme } from "@mui/material"; import type { FunctionComponent } from "react"; @@ -170,7 +169,8 @@ export const Topbar = ({ const { flowDefinitions, selectedFlowDefinitionId } = useFlowDefinitionsContext(); - const { flowRuns, selectedFlowRunId, selectedFlowRun } = useFlowRunsContext(); + const { flowRuns, selectedFlowRunEntityId, selectedFlowRun } = + useFlowRunsContext(); const [cancelFlow] = useMutation< CancelFlowMutation, @@ -178,11 +178,13 @@ export const Topbar = ({ >(cancelFlowMutation); const onCancelFlowClicked = useCallback(async () => { - if (selectedFlowRunId) { + if (selectedFlowRunEntityId) { setCancelling(true); try { - await cancelFlow({ variables: { flowUuid: selectedFlowRunId } }); + await cancelFlow({ + variables: { flowRunEntityId: selectedFlowRunEntityId }, + }); } catch { /** * We don't nee to worry about the success case because the cancel flow button will disappear @@ -190,7 +192,7 @@ export const Topbar = ({ setCancelling(false); } } - }, [cancelFlow, selectedFlowRunId]); + }, [cancelFlow, selectedFlowRunEntityId]); const onRunFlowClicked = useCallback(() => { handleRunFlowClicked(); @@ -290,7 +292,7 @@ export const Topbar = ({ ) : ( { - /** - * @todo update this to use the flow definition's uuid when stored in the db - * also then needs to take account of the correct namespace, which might be different from the - * current - */ - void push( - generateFlowDefinitionPath({ - shortname: "hash", - flowDefinitionId: event.target.value, - }), + const flow = flowDefinitions.find( + ({ flowDefinitionEntityId }) => + flowDefinitionEntityId === event.target.value, ); + + if (flow) { + void push(getFlowDefinitionPath(flow.flowDefinitionEntityId)); + } }} > {flowDefinitions.map((flow) => ( {flow.flowDefinition.name} @@ -297,12 +305,11 @@ export const Topbar = ({ const value = event.target.value; if (value === "none") { - void push( - generateFlowDefinitionPath({ - shortname: "hash", - flowDefinitionId: selectedFlowDefinitionId ?? "none", - }), - ); + if (selectedFlowDefinitionEntityId) { + void push( + getFlowDefinitionPath(selectedFlowDefinitionEntityId), + ); + } return; } diff --git a/apps/hash-frontend/src/pages/@/[shortname]/workers/[run-id].page.tsx b/apps/hash-frontend/src/pages/@/[shortname]/workers/[run-id].page.tsx index 755f8b38a51..3adb2a30d37 100644 --- a/apps/hash-frontend/src/pages/@/[shortname]/workers/[run-id].page.tsx +++ b/apps/hash-frontend/src/pages/@/[shortname]/workers/[run-id].page.tsx @@ -24,7 +24,7 @@ const WorkerFlowDefinitionResolver = () => { return ( 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 60be2707101..a7abe783a40 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 @@ -12,6 +12,7 @@ import { systemPropertyTypes } from "@local/hash-isomorphic-utils/ontology-type- import { FlowRunStatus } from "../../../graphql/api-types.gen"; import { updateEntityMutation } from "../../../graphql/queries/knowledge/entity.queries"; import { startFlowMutation } from "../../../graphql/queries/knowledge/flow.queries"; +import { useGenerateHashFlowDefinitionEntityId } from "../../shared/use-hash-flows"; import type { FlowRun, @@ -205,6 +206,9 @@ export const useDashboardItemConfig = ({ UpdateEntityMutationVariables >(updateEntityMutation); + const generateHashFlowDefinitionEntityId = + useGenerateHashFlowDefinitionEntityId(); + const [startFlow] = useMutation< StartFlowMutation, StartFlowMutationVariables @@ -600,7 +604,9 @@ export const useDashboardItemConfig = ({ // Start the flow const { data } = await startFlow({ variables: { - flowDefinitionId: flowDefinitionIds.configureDashboardItem, + flowDefinitionEntityId: generateHashFlowDefinitionEntityId( + flowDefinitionIds.configureDashboardItem, + ), flowInputs: { userGoal: { kind: "Text", @@ -664,6 +670,7 @@ export const useDashboardItemConfig = ({ onGenerationStarted, onComplete, markGenerationFailed, + generateHashFlowDefinitionEntityId, ]); const refineConfiguration = useCallback( @@ -718,7 +725,9 @@ export const useDashboardItemConfig = ({ const { data } = await startFlow({ variables: { - flowDefinitionId: flowDefinitionIds.refineDashboardItem, + flowDefinitionEntityId: generateHashFlowDefinitionEntityId( + flowDefinitionIds.refineDashboardItem, + ), flowInputs: { userGoal: { kind: "Text", value: initialGoal }, refinementInstruction: { @@ -789,6 +798,7 @@ export const useDashboardItemConfig = ({ initialValues, markGenerationFailed, onGenerationStarted, + generateHashFlowDefinitionEntityId, setError, startFlow, startPolling, diff --git a/apps/hash-frontend/src/pages/flows.page.tsx b/apps/hash-frontend/src/pages/flows.page.tsx index bc221aa18ce..eba33acc05a 100644 --- a/apps/hash-frontend/src/pages/flows.page.tsx +++ b/apps/hash-frontend/src/pages/flows.page.tsx @@ -3,9 +3,10 @@ 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 { isGoalFlowDefinitionEntityId } from "@local/hash-isomorphic-utils/flows/flow-definition-ids"; import { generateFlowDefinitionPath } from "@local/hash-isomorphic-utils/flows/frontend-paths"; +import { useGetOwnerForEntity } from "../components/hooks/use-get-owner-for-entity"; import { getLayoutWithSidebar } from "../shared/layout"; import { Link } from "../shared/ui/link"; import { WorkersHeader } from "../shared/workers-header"; @@ -28,6 +29,7 @@ import type { VirtualizedTableRow, } from "./shared/virtualized-table"; import type { VirtualizedTableSort } from "./shared/virtualized-table/header/sort"; +import type { EntityId } from "@blockprotocol/type-system"; import type { Subtype } from "@local/advanced-types/subtype"; type FieldId = "web" | "name" | "description" | "lastRunStartedAt"; @@ -69,14 +71,15 @@ type FlowSummary = Subtype< isOrg: boolean; }; name: string; - uuid: string; + flowDefinitionEntityId: EntityId; description: string; lastRunStartedAt: string | null; } >; const TableRow = memo(({ flowSummary }: { flowSummary: FlowSummary }) => { - const { web, name, uuid, description, lastRunStartedAt } = flowSummary; + const { web, name, flowDefinitionEntityId, description, lastRunStartedAt } = + flowSummary; return ( <> @@ -87,7 +90,7 @@ const TableRow = memo(({ flowSummary }: { flowSummary: FlowSummary }) => { { const { flowRuns } = useFlowRunsContext(); + const getOwner = useGetOwnerForEntity(); + const flowDefinitionRows = useMemo[]>(() => { const rowData: VirtualizedTableRow[] = allFlowDefinitions .filter( - ({ flowDefinitionId }) => !isGoalFlowDefinitionId(flowDefinitionId), + ({ flowDefinitionEntityId }) => + !isGoalFlowDefinitionEntityId(flowDefinitionEntityId), ) - .map(({ flowDefinitionId, flowDefinition }) => { + .map(({ flowDefinitionEntityId, flowDefinition }) => { let lastRunStartedAt = null; for (const flowRun of flowRuns) { if ( - flowRun.flowDefinitionId === flowDefinitionId && + flowRun.flowDefinitionEntityId === flowDefinitionEntityId && (!lastRunStartedAt || flowRun.startedAt > lastRunStartedAt) ) { lastRunStartedAt = flowRun.startedAt; @@ -151,16 +157,17 @@ const FlowsPageContent = () => { } return { - id: flowDefinitionId, + id: flowDefinitionEntityId, data: { web: { avatarUrl: "/hash-logo-black.png", name: "HASH", - shortname: "hash", + shortname: getOwner({ entityId: flowDefinitionEntityId }) + .shortname, isOrg: true, }, name: flowDefinition.name, - uuid: flowDefinitionId, + flowDefinitionEntityId, description: flowDefinition.description, lastRunStartedAt, }, @@ -179,7 +186,7 @@ const FlowsPageContent = () => { (a.data[field] ?? "").localeCompare(b.data[field] ?? "") * direction ); }); - }, [allFlowDefinitions, flowRuns, sort]); + }, [allFlowDefinitions, flowRuns, getOwner, sort]); const tableHeight = Math.min( 600, @@ -216,7 +223,7 @@ const FlowsPageContent = () => { const FlowsPage: NextPageWithLayout = () => { return ( - + 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 b502f4b1315..a89def3aa88 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/flow-definition-ids"; +import { isGoalFlowDefinitionEntityId } from "@local/hash-isomorphic-utils/flows/flow-definition-ids"; import { useAuthenticatedUser } from "../shared/auth-info-context"; import { useFlowRunsContext } from "../shared/flow-runs-context"; @@ -9,7 +9,6 @@ import { flowRunStatusToStatusText } from "../shared/flow-tables"; import { GoalListSection } from "./goals-list/goal-list-section"; import type { GoalSummary } from "./goals-list/goal-list-section/goal-row"; -import type { EntityUuid } from "@blockprotocol/type-system"; export const GoalsList = () => { const { flowRuns, loading } = useFlowRunsContext(); @@ -26,7 +25,7 @@ export const GoalsList = () => { const webByWebId: Record = {}; for (const run of flowRuns) { - if (!isGoalFlowDefinitionId(run.flowDefinitionId as EntityUuid)) { + if (!isGoalFlowDefinitionEntityId(run.flowDefinitionEntityId)) { continue; } diff --git a/apps/hash-frontend/src/pages/goals/new.page.tsx b/apps/hash-frontend/src/pages/goals/new.page.tsx index 3f26caf3980..39118041097 100644 --- a/apps/hash-frontend/src/pages/goals/new.page.tsx +++ b/apps/hash-frontend/src/pages/goals/new.page.tsx @@ -35,6 +35,7 @@ import { Button } from "../../shared/ui/button"; import { WorkersHeader } from "../../shared/workers-header"; import { useAuthenticatedUser } from "../shared/auth-info-context"; import { EntityTypeSelector } from "../shared/entity-type-selector"; +import { useGenerateHashFlowDefinitionEntityId } from "../shared/use-hash-flows"; import { WebSelector } from "../shared/web-selector"; import { defaultBrowserPluginDomains } from "./new.page/default-browser-plugin-domains"; import { DeliverableSettings } from "./new.page/deliverable-settings"; @@ -189,6 +190,9 @@ const NewGoalPageContent = () => { const getOwner = useGetOwnerForEntity(); const { push } = useRouter(); + const generateHashFlowDefinitionEntityId = + useGenerateHashFlowDefinitionEntityId(); + const [startFlow, { called }] = useMutation< StartFlowMutation, StartFlowMutationVariables @@ -284,7 +288,9 @@ const NewGoalPageContent = () => { }, internetAccess: internetSettings, }, - flowDefinitionId: flowDefinitionIds.goal, + flowDefinitionEntityId: generateHashFlowDefinitionEntityId( + 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 b5ff0cae3c4..96cb74211c5 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 @@ -4,22 +4,22 @@ 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 { EntityId } from "@blockprotocol/type-system"; import type { PropsWithChildren } from "react"; export const FlowDefinitionsContextProvider = ({ children, - selectedFlowDefinitionId, -}: PropsWithChildren<{ selectedFlowDefinitionId: EntityUuid | null }>) => { + selectedFlowDefinitionEntityId, +}: PropsWithChildren<{ selectedFlowDefinitionEntityId: EntityId | null }>) => { const { flowDefinitions, loading } = useFlowDefinitions(); const context = useMemo( () => ({ flowDefinitions, loading, - selectedFlowDefinitionId, + selectedFlowDefinitionEntityId, }), - [flowDefinitions, loading, selectedFlowDefinitionId], + [flowDefinitions, loading, selectedFlowDefinitionEntityId], ); 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 a9b9ca439ff..41a29f59243 100644 --- a/apps/hash-frontend/src/pages/shared/flow-definitions-context.ts +++ b/apps/hash-frontend/src/pages/shared/flow-definitions-context.ts @@ -1,13 +1,13 @@ import { createContext, useContext } from "react"; -import type { EntityUuid } from "@blockprotocol/type-system"; -import type { FlowDefinitionWithId } from "@local/hash-isomorphic-utils/flows/types"; +import type { LoadedFlowDefinition } from "./use-flow-definitions"; +import type { EntityId } from "@blockprotocol/type-system"; export type FlowDefinitionsContextType = { /** The current edition of every flow definition the user can view, from the graph. */ - flowDefinitions: FlowDefinitionWithId[]; + flowDefinitions: LoadedFlowDefinition[]; loading: boolean; - selectedFlowDefinitionId: EntityUuid | null; + selectedFlowDefinitionEntityId: EntityId | null; }; export const FlowDefinitionsContext = diff --git a/apps/hash-frontend/src/pages/shared/use-flow-definitions.ts b/apps/hash-frontend/src/pages/shared/use-flow-definitions.ts index 6f1a8aa9ca7..dde3c18ac1d 100644 --- a/apps/hash-frontend/src/pages/shared/use-flow-definitions.ts +++ b/apps/hash-frontend/src/pages/shared/use-flow-definitions.ts @@ -2,7 +2,7 @@ import { useQuery } from "@apollo/client"; import { useMemo } from "react"; import { getRoots } from "@blockprotocol/graph/stdlib"; -import { extractEntityUuidFromEntityId } from "@blockprotocol/type-system"; +import { splitEntityId } from "@blockprotocol/type-system"; import { deserializeSubgraph } from "@local/hash-graph-sdk/subgraph"; import { getFlowDefinitionFromEntity } from "@local/hash-isomorphic-utils/flows/mappings"; import { @@ -19,15 +19,23 @@ import type { QueryEntitySubgraphQueryVariables, } from "../../graphql/api-types.gen"; import type { EntityRootType } from "@blockprotocol/graph"; -import type { EntityUuid, Timestamp } from "@blockprotocol/type-system"; +import type { EntityId, 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 } from "@local/hash-isomorphic-utils/flows/types"; import type { FlowDefinition as FlowDefinitionEntity } from "@local/hash-isomorphic-utils/system-types/flowdefinition"; +/** + * A flow definition loaded from the graph, with the entity id of the `Flow Definition` entity that holds it. + */ +export type LoadedFlowDefinition = { + flowDefinitionEntityId: EntityId; + flowDefinition: FlowDefinition; +}; + const flowDefinitionsQueryVariables = ( filter: Filter, temporalAxes: QueryTemporalAxesUnresolved, @@ -59,7 +67,7 @@ const flowDefinitionsQueryVariables = ( */ const getFlowDefinitionsFromQueryResult = ( data: QueryEntitySubgraphQuery | undefined, -): FlowDefinitionWithId[] => { +): LoadedFlowDefinition[] => { if (!data) { return []; } @@ -69,19 +77,17 @@ const getFlowDefinitionsFromQueryResult = ( >(data.queryEntitySubgraph.subgraph); return getRoots(subgraph).flatMap((entity) => { - const flowDefinitionId = extractEntityUuidFromEntityId( - entity.metadata.recordId.entityId, - ); + const flowDefinitionEntityId = entity.metadata.recordId.entityId; try { return { - flowDefinitionId, + flowDefinitionEntityId, 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}`, + `Flow definition ${flowDefinitionEntityId} is invalid, so isn't shown: ${(error as Error).message}`, ); return []; } @@ -92,7 +98,7 @@ const getFlowDefinitionsFromQueryResult = ( * The current edition of every flow definition the user can view. */ export const useFlowDefinitions = (): { - flowDefinitions: FlowDefinitionWithId[]; + flowDefinitions: LoadedFlowDefinition[]; loading: boolean; } => { const { data, loading } = useQuery< @@ -118,22 +124,31 @@ export const useFlowDefinitions = (): { * 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; + flowDefinitionEntityId: EntityId | null; timestamp: Timestamp | null; -}): { flowDefinition: FlowDefinitionWithId | null; loading: boolean } => { - const { flowDefinitionId, timestamp } = params; +}): { flowDefinition: LoadedFlowDefinition | null; loading: boolean } => { + const { flowDefinitionEntityId, timestamp } = params; + + const [webId, entityUuid] = flowDefinitionEntityId + ? splitEntityId(flowDefinitionEntityId) + : []; const { data, loading } = useQuery< QueryEntitySubgraphQuery, QueryEntitySubgraphQueryVariables >(queryEntitySubgraphQuery, { variables: flowDefinitionsQueryVariables( - { equal: [{ path: ["uuid"] }, { parameter: flowDefinitionId }] }, + { + all: [ + { equal: [{ path: ["webId"] }, { parameter: webId }] }, + { equal: [{ path: ["uuid"] }, { parameter: entityUuid }] }, + ], + }, timestamp ? generateTimeInstantTemporalAxes(timestamp) : currentTimeInstantTemporalAxes, ), - skip: !flowDefinitionId || !timestamp, + skip: !flowDefinitionEntityId || !timestamp, fetchPolicy: "cache-first", }); diff --git a/apps/hash-frontend/src/pages/shared/use-hash-flows.ts b/apps/hash-frontend/src/pages/shared/use-hash-flows.ts new file mode 100644 index 00000000000..dd9a33e2e35 --- /dev/null +++ b/apps/hash-frontend/src/pages/shared/use-hash-flows.ts @@ -0,0 +1,30 @@ +import { useCallback } from "react"; + +import { extractWebIdFromEntityId } from "@blockprotocol/type-system"; +import { generateHashFlowDefinitionEntityId } from "@local/hash-isomorphic-utils/flows/flow-definition-ids"; + +import { useHashInstance } from "../../components/hooks/use-hash-instance"; + +import type { EntityId, EntityUuid } from "@blockprotocol/type-system"; + +/** + * Generates the entity id of one of the flows defined in code, from its id in `flowDefinitionIds`, to start a run of + * it. They're in the web the `HASH Instance` entity is in. + */ +export const useGenerateHashFlowDefinitionEntityId = () => { + const { hashInstance } = useHashInstance(); + + return useCallback( + (flowDefinitionId: EntityUuid): EntityId => { + if (!hashInstance) { + throw new Error("The HASH instance hasn't loaded"); + } + + return generateHashFlowDefinitionEntityId( + extractWebIdFromEntityId(hashInstance.metadata.recordId.entityId), + flowDefinitionId, + ); + }, + [hashInstance], + ); +}; diff --git a/apps/hash-frontend/src/pages/workers.page.tsx b/apps/hash-frontend/src/pages/workers.page.tsx index 1a605ab189a..5df405f5837 100644 --- a/apps/hash-frontend/src/pages/workers.page.tsx +++ b/apps/hash-frontend/src/pages/workers.page.tsx @@ -20,7 +20,7 @@ const WorkersPageContent = () => { query: { [workerFlowFilterParam]: definitionFilterQueryParam }, } = useRouter(); - const flowDefinitionIdFilter = useMemo(() => { + const flowDefinitionEntityIdFilter = useMemo(() => { if (!definitionFilterQueryParam) { return undefined; } @@ -48,7 +48,9 @@ const WorkersPageContent = () => { Flow Runs - + 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 189fadac2fc..fa3b7092ee6 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,7 +14,7 @@ import { InfinityLightIcon, Skeleton, } from "@hashintel/design-system"; -import { isGoalFlowDefinitionId } from "@local/hash-isomorphic-utils/flows/flow-definition-ids"; +import { isGoalFlowDefinitionEntityId } from "@local/hash-isomorphic-utils/flows/flow-definition-ids"; import { generateWorkerRunPath } from "@local/hash-isomorphic-utils/flows/frontend-paths"; import { Link } from "../../shared/ui/link"; @@ -271,10 +271,12 @@ const LoadingComponent = ({ columnCount }: { columnCount: number }) => ( ); type FlowRunTableProps = { - flowDefinitionIdFilter?: string[]; + flowDefinitionEntityIdFilter?: string[]; }; -export const FlowRunTable = ({ flowDefinitionIdFilter }: FlowRunTableProps) => { +export const FlowRunTable = ({ + flowDefinitionEntityIdFilter, +}: FlowRunTableProps) => { const [sort, setSort] = useState>({ fieldId: "closedAt", direction: "desc", @@ -291,13 +293,16 @@ export const FlowRunTable = ({ flowDefinitionIdFilter }: FlowRunTableProps) => { const { authenticatedUser } = useAuthenticatedUser(); const filteredFlowRuns = useMemo(() => { - if (!flowDefinitionIdFilter || flowDefinitionIdFilter.length === 0) { + if ( + !flowDefinitionEntityIdFilter || + flowDefinitionEntityIdFilter.length === 0 + ) { return unfilteredFlowRuns; } return unfilteredFlowRuns.filter((run) => - flowDefinitionIdFilter.includes(run.flowDefinitionId), + flowDefinitionEntityIdFilter.includes(run.flowDefinitionEntityId), ); - }, [unfilteredFlowRuns, flowDefinitionIdFilter]); + }, [unfilteredFlowRuns, flowDefinitionEntityIdFilter]); const { isUsageAvailable: usageAvailable, usageByFlowRun } = useFlowRunsUsage( { @@ -310,8 +315,8 @@ export const FlowRunTable = ({ flowDefinitionIdFilter }: FlowRunTableProps) => { const rowData: VirtualizedTableRow[] = filteredFlowRuns.map( (flowRun) => { - const type = isGoalFlowDefinitionId( - flowRun.flowDefinitionId as EntityUuid, + const type = isGoalFlowDefinitionEntityId( + flowRun.flowDefinitionEntityId, ) ? "goal" : "flow"; @@ -417,10 +422,10 @@ export const FlowRunTable = ({ flowDefinitionIdFilter }: FlowRunTableProps) => { () => ( ), - [columns, flowDefinitionIdFilter], + [columns, flowDefinitionEntityIdFilter], ); const LoadingPlaceholder = useCallback( diff --git a/apps/plugin-browser/src/graphql/queries/flow.queries.ts b/apps/plugin-browser/src/graphql/queries/flow.queries.ts index 24de8766b42..9ffe25a70df 100644 --- a/apps/plugin-browser/src/graphql/queries/flow.queries.ts +++ b/apps/plugin-browser/src/graphql/queries/flow.queries.ts @@ -4,7 +4,7 @@ export const getMinimalFlowRunsQuery = /* GraphQL */ ` totalCount flowRuns { name - flowDefinitionId + flowDefinitionEntityId flowRunEntityId webId status diff --git a/apps/plugin-browser/src/graphql/queries/user.queries.ts b/apps/plugin-browser/src/graphql/queries/user.queries.ts index 3765a2c6da0..1e4d3b7b370 100644 --- a/apps/plugin-browser/src/graphql/queries/user.queries.ts +++ b/apps/plugin-browser/src/graphql/queries/user.queries.ts @@ -9,6 +9,9 @@ export const meQuery = /* GraphQL */ ` ...SubgraphFields } } + hashInstanceSettings { + entity + } } ${print(subgraphFieldsFragment)} `; 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 4ee5b937b23..bfc6614de6c 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,10 @@ import { TableCell, TableRow } from "@mui/material"; import { useMemo } from "react"; -import { flowDefinitionIds } from "@local/hash-isomorphic-utils/flows/flow-definition-ids"; +import { + flowDefinitionIds, + isHashFlowDefinitionEntityId, +} from "@local/hash-isomorphic-utils/flows/flow-definition-ids"; import { EventTable } from "./shared/event-table"; import { HistoryRow } from "./shared/history-row"; @@ -18,9 +21,10 @@ export const AutomaticallyTriggered = ({ () => unfilteredFlowRuns.filter( (flow) => - flow.flowDefinitionId === - flowDefinitionIds.automaticBrowserInference || - flow.requestedPageUrl, + isHashFlowDefinitionEntityId( + flow.flowDefinitionEntityId, + 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 386212e2462..9b516e8469e 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,10 @@ import { TableCell, TableRow } from "@mui/material"; import { useMemo } from "react"; -import { flowDefinitionIds } from "@local/hash-isomorphic-utils/flows/flow-definition-ids"; +import { + flowDefinitionIds, + isHashFlowDefinitionEntityId, +} from "@local/hash-isomorphic-utils/flows/flow-definition-ids"; import { EventTable } from "./shared/event-table"; import { HistoryRow } from "./shared/history-row"; @@ -16,9 +19,11 @@ export const ManuallyTriggered = ({ }) => { const manuallyTriggeredFlows = useMemo( () => - unfilteredFlowRuns.filter( - (flow) => - flow.flowDefinitionId === flowDefinitionIds.manualBrowserInference, + unfilteredFlowRuns.filter((flow) => + isHashFlowDefinitionEntityId( + flow.flowDefinitionEntityId, + 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 32e93187322..5d30c19fdab 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,21 @@ import { Link, Stack, Typography } from "@mui/material"; import { ArrowUpRightRegularIcon } from "@hashintel/design-system"; -import { flowDefinitionIds } from "@local/hash-isomorphic-utils/flows/flow-definition-ids"; +import { + flowDefinitionIds, + generateHashFlowDefinitionEntityId, +} from "@local/hash-isomorphic-utils/flows/flow-definition-ids"; import { generatedFilteredWorkersPath } from "@local/hash-isomorphic-utils/flows/frontend-paths"; +import { useUserContext } from "../../../shared/user-context"; + export const TableLabel = ({ type }: { type: "manual" | "automatic" }) => { + const { user } = useUserContext(); + + if (!user) { + return null; + } + return ( @@ -12,10 +23,14 @@ export const TableLabel = ({ type }: { type: "manual" | "automatic" }) => { [ ...(existingValue ?? []), { - flowDefinitionId, + flowDefinitionEntityId, flowRunEntityId: entityIdFromComponents(webId, requestUuid), closedAt: null, executedAt: new Date().toISOString(), diff --git a/apps/plugin-browser/src/shared/get-user.ts b/apps/plugin-browser/src/shared/get-user.ts index 77ee9d13a67..470719f8ac8 100644 --- a/apps/plugin-browser/src/shared/get-user.ts +++ b/apps/plugin-browser/src/shared/get-user.ts @@ -4,6 +4,7 @@ import { intervalForTimestamp, } from "@blockprotocol/graph/stdlib"; import { currentTimestamp } from "@blockprotocol/type-system"; +import { HashEntity } from "@local/hash-graph-sdk/entity"; import { mapGqlSubgraphFieldsFragmentToSubgraph } from "@local/hash-isomorphic-utils/graph-queries"; import { systemEntityTypes, @@ -21,7 +22,6 @@ import type { MeQuery, MeQueryVariables } from "../graphql/api-types.gen"; import type { LocalStorage } from "./storage"; import type { EntityRootType, Subgraph } from "@blockprotocol/graph"; import type { Entity, EntityId, WebId } from "@blockprotocol/type-system"; -import type { HashEntity } from "@local/hash-graph-sdk/entity"; import type { FeatureFlag } from "@local/hash-isomorphic-utils/feature-flags"; import type { ImageFile } from "@local/hash-isomorphic-utils/system-types/imagefile"; import type { @@ -69,6 +69,16 @@ export const getUser = (): Promise => { const user = getRoots(subgraph)[0]!; + const hashInstanceEntity = data.hashInstanceSettings?.entity; + + if (!hashInstanceEntity) { + throw new Error("The API returned no HASH instance settings"); + } + + const hashWebId = getWebIdFromEntityId( + new HashEntity(hashInstanceEntity).metadata.recordId.entityId, + ); + const simpleProperties = simplifyProperties( user.properties as UserProperties, ); @@ -305,6 +315,7 @@ export const getUser = (): Promise => { enabledFeatureFlags, settingsEntityId, webWebId: getWebIdFromEntityId(user.metadata.recordId.entityId), + hashWebId, } as LocalStorage["user"]; }) .catch((error) => { diff --git a/apps/plugin-browser/src/shared/storage.ts b/apps/plugin-browser/src/shared/storage.ts index 2eb15629ffd..5e9c3e4775a 100644 --- a/apps/plugin-browser/src/shared/storage.ts +++ b/apps/plugin-browser/src/shared/storage.ts @@ -57,6 +57,8 @@ type UserAndLinkedData = SimplifiedUser & { })[]; settingsEntityId: EntityId; webWebId: WebId; + /** The web HASH's own flow definitions are in: the web the `HASH Instance` entity is in. */ + hashWebId: WebId; }; const persistedUserSettingKeys = [ @@ -92,7 +94,7 @@ export type ExternalInputRequestById = { export type MinimalFlowRun = Pick< FlowRun, - | "flowDefinitionId" + | "flowDefinitionEntityId" | "flowRunEntityId" | "webId" | "closedAt" diff --git a/libs/@local/hash-backend-utils/src/flows.ts b/libs/@local/hash-backend-utils/src/flows.ts index 8aa287687cc..94e5d2919ba 100644 --- a/libs/@local/hash-backend-utils/src/flows.ts +++ b/libs/@local/hash-backend-utils/src/flows.ts @@ -1,4 +1,7 @@ -import { extractWebIdFromEntityId } from "@blockprotocol/type-system"; +import { + extractEntityUuidFromEntityId, + extractWebIdFromEntityId, +} from "@blockprotocol/type-system"; import { typedKeys } from "@local/advanced-types/typed-entries"; import { queryEntities, summarizeEntities } from "@local/hash-graph-sdk/entity"; import { flowRunsQueryMaxLimit } from "@local/hash-isomorphic-utils/flows/types"; @@ -99,17 +102,17 @@ export async function getFlowRunById({ }) ).get(flowRunEntityId); - if (!links?.flowDefinitionId) { + if (!links?.flowDefinitionEntityId) { /* A run from before flow runs linked to their definition: it can't be shown. */ return null; } - const flowDefinitionId = links.flowDefinitionId; + const flowDefinitionEntityId = links.flowDefinitionEntityId; const flowScheduleId = links.flowTriggerId ?? undefined; if (includeDetails) { return getFlowRunFromTemporalWorkflowId({ - flowDefinitionId, + flowDefinitionEntityId, flowRunEntityId, flowScheduleId, name, @@ -120,7 +123,7 @@ export async function getFlowRunById({ }); } else { return getSparseFlowRunFromTemporalWorkflowId({ - flowDefinitionId, + flowDefinitionEntityId, flowRunEntityId, flowScheduleId, name, @@ -141,7 +144,7 @@ const convertScreamingSnakeToPascalCase = (str: string) => type GetFlowRunsFilters = { executionStatus?: FlowRunStatus | null; - flowDefinitionIds?: string[] | null; + flowDefinitionEntityIds?: EntityId[] | null; cursor?: string | null; limit?: number | null; }; @@ -156,7 +159,7 @@ type GetFlowRunsFnArgs = { }; type MinimalFlowMetadata = { - flowDefinitionId: EntityUuid; + flowDefinitionEntityId: EntityId; flowRunEntityId: EntityId; flowScheduleId?: EntityUuid; name: string; @@ -211,19 +214,25 @@ export async function getFlowRuns({ }, ], }, - ...(filters.flowDefinitionIds + ...(filters.flowDefinitionEntityIds ? [ { /** - * A run `Uses` the flow definition it runs. Flow definition ids are the uuids of their entities, and - * no other entity a run links to shares one. + * A run `Uses` the flow definition it runs. Matched by the definition's uuid here, and by its full + * entity id when the run's links are read below. */ - any: filters.flowDefinitionIds.map((flowDefinitionId) => ({ - equal: [ - { path: ["outgoingLinks", "rightEntity", "uuid"] }, - { parameter: flowDefinitionId }, - ], - })), + any: filters.flowDefinitionEntityIds.map( + (flowDefinitionEntityId) => ({ + equal: [ + { path: ["outgoingLinks", "rightEntity", "uuid"] }, + { + parameter: extractEntityUuidFromEntityId( + flowDefinitionEntityId, + ), + }, + ], + }), + ), }, ] : []), @@ -278,7 +287,7 @@ export async function getFlowRuns({ const links = linksByFlowRunEntityId.get(flowRunEntityId); - if (!links?.flowDefinitionId) { + if (!links?.flowDefinitionEntityId) { /* A run from before flow runs linked to their definition: it can't be shown. */ continue; } @@ -288,8 +297,8 @@ export async function getFlowRuns({ * one of the requested definitions. A page can then hold fewer runs than its limit. */ if ( - filters.flowDefinitionIds && - !filters.flowDefinitionIds.includes(links.flowDefinitionId) + filters.flowDefinitionEntityIds && + !filters.flowDefinitionEntityIds.includes(links.flowDefinitionEntityId) ) { continue; } @@ -303,7 +312,7 @@ export async function getFlowRuns({ entity.properties["https://hash.ai/@h/types/property-type/workflow-id/"]; temporalWorkflowIdToFlowDetails[temporalWorkflowId] = { - flowDefinitionId: links.flowDefinitionId, + flowDefinitionEntityId: links.flowDefinitionEntityId, flowRunEntityId, flowScheduleId: links.flowTriggerId ?? undefined, name, @@ -374,7 +383,7 @@ export async function getFlowRuns({ const flowDetails = temporalWorkflowIdToFlowDetails[temporalWorkflowId]!; const runInfo = await getFlowRunFromTemporalWorkflowId({ - flowDefinitionId: flowDetails.flowDefinitionId, + flowDefinitionEntityId: flowDetails.flowDefinitionEntityId, flowRunEntityId: flowDetails.flowRunEntityId, flowScheduleId: flowDetails.flowScheduleId, name: flowDetails.name, @@ -394,7 +403,7 @@ export async function getFlowRuns({ const flowDetails = temporalWorkflowIdToFlowDetails[temporalWorkflowId]!; const runInfo = await getSparseFlowRunFromTemporalWorkflowId({ - flowDefinitionId: flowDetails.flowDefinitionId, + flowDefinitionEntityId: flowDetails.flowDefinitionEntityId, flowRunEntityId: flowDetails.flowRunEntityId, flowScheduleId: flowDetails.flowScheduleId, name: flowDetails.name, diff --git a/libs/@local/hash-backend-utils/src/flows/get-flow-run-details.ts b/libs/@local/hash-backend-utils/src/flows/get-flow-run-details.ts index 5dfb0c70be1..fc67ed2ca7d 100644 --- a/libs/@local/hash-backend-utils/src/flows/get-flow-run-details.ts +++ b/libs/@local/hash-backend-utils/src/flows/get-flow-run-details.ts @@ -746,7 +746,7 @@ const getFlowRunDetailedFields = async ({ }; export const getSparseFlowRunFromTemporalWorkflowId = async ({ - flowDefinitionId, + flowDefinitionEntityId, flowRunEntityId, flowScheduleId, name, @@ -755,7 +755,7 @@ export const getSparseFlowRunFromTemporalWorkflowId = async ({ webId, }: { /** The flow definition the run `Uses`, from its link */ - flowDefinitionId: EntityUuid; + flowDefinitionEntityId: EntityId; /** The entity id of the flow run */ flowRunEntityId: EntityId; /** The schedule the run was `Triggered By`, if any, from its link */ @@ -773,7 +773,7 @@ export const getSparseFlowRunFromTemporalWorkflowId = async ({ return { name, - flowDefinitionId, + flowDefinitionEntityId, flowRunEntityId, flowScheduleId, status: status.name as FlowRunStatus, @@ -786,7 +786,7 @@ export const getSparseFlowRunFromTemporalWorkflowId = async ({ export const getFlowRunFromTemporalWorkflowId = async (args: { /** The flow definition the run `Uses`, from its link */ - flowDefinitionId: EntityUuid; + flowDefinitionEntityId: EntityId; /** The entity id of the flow run */ flowRunEntityId: EntityId; /** The schedule the run was `Triggered By`, if any, from its link */ 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 e00ca4e7a0e..e1a175d9897 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 @@ -38,7 +38,7 @@ import type { CreateFlowActivities, ProxyFlowActivity, } from "./action-types.js"; -import type { EntityUuid, Timestamp } from "@blockprotocol/type-system"; +import type { EntityId, Timestamp } from "@blockprotocol/type-system"; import type { FlowActionStepOutput } from "@local/hash-isomorphic-utils/flows/action-definitions"; import type { BaseRunFlowWorkflowParams, @@ -299,7 +299,7 @@ const processFlow = async < params: BaseRunFlowWorkflowParams & { generateFlowRunName?: (params: { flowDefinition: FlowDefinition; - flowDefinitionId: EntityUuid; + flowDefinitionEntityId: EntityId; flowInputs: FlowInputValues; }) => Promise; proxyFlowActivity: ProxyFlowActivity< @@ -309,7 +309,7 @@ const processFlow = async < }, ): Promise => { const { - flowDefinitionId, + flowDefinitionEntityId, flowRunEntityUuid, flowRunName, flowInputs, @@ -326,7 +326,7 @@ const processFlow = async < */ const flowDefinitionResult = await getFlowDefinitionActivity({ atTime: workflowInfo().startTime.toISOString() as Timestamp, - flowDefinitionId, + flowDefinitionEntityId, userAuthentication, }); @@ -380,7 +380,7 @@ const processFlow = async < const flow = initializeFlow({ flowDefinition, - flowDefinitionId, + flowDefinitionEntityId, flowInputs, temporalWorkflowId: workflowId, /** @@ -403,7 +403,7 @@ const processFlow = async < if (generateFlowRunName) { const generatedName = await generateFlowRunName({ flowDefinition, - flowDefinitionId, + flowDefinitionEntityId, flowInputs, }); flow.name = generatedName; 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 613110b82ed..386be99060c 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 @@ -5,7 +5,6 @@ import { getFlowDefinitionEntityById } from "../../shared/get-flow-definition-en import type { ActorEntityUuid, EntityId, - EntityUuid, Timestamp, } from "@blockprotocol/type-system"; import type { GraphApi } from "@local/hash-graph-client"; @@ -14,7 +13,7 @@ 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; + flowDefinitionEntityId: EntityId; userAuthentication: { actorId: ActorEntityUuid }; }; @@ -32,12 +31,12 @@ export const getFlowDefinitionActivity = async ( } | { status: "not-found" | "invalid"; errorMessage: string } > => { - const { atTime, flowDefinitionId, graphApiClient, userAuthentication } = + const { atTime, flowDefinitionEntityId, graphApiClient, userAuthentication } = params; const entity = await getFlowDefinitionEntityById({ atTime, - flowDefinitionId, + flowDefinitionEntityId, graphApiClient, userAuthentication, }); @@ -45,7 +44,7 @@ export const getFlowDefinitionActivity = async ( if (!entity) { return { status: "not-found", - errorMessage: `No Flow Definition entity with uuid ${flowDefinitionId} exists, or the user can't view it`, + errorMessage: `No Flow Definition entity ${flowDefinitionEntityId} exists, or the user can't view it`, }; } diff --git a/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/initialize-flow.ts b/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/initialize-flow.ts index 614ab8acb68..578254ed96a 100644 --- a/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/initialize-flow.ts +++ b/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/initialize-flow.ts @@ -2,7 +2,7 @@ import { actionDefinitions } from "@local/hash-isomorphic-utils/flows/action-def import { getAllStepsInFlow } from "./get-all-steps-in-flow.js"; -import type { EntityUuid } from "@blockprotocol/type-system"; +import type { EntityId } from "@blockprotocol/type-system"; import type { ActionStep, ActionStepDefinition, @@ -167,14 +167,14 @@ export const initializeForEachStep = (params: { export const initializeFlow = (params: { flowDefinition: FlowDefinition; - flowDefinitionId: EntityUuid; + flowDefinitionEntityId: EntityId; flowInputs: FlowInputValues; name: string; temporalWorkflowId: string; }): LocalFlowRun => { const { flowDefinition, - flowDefinitionId, + flowDefinitionEntityId, flowInputs, name, temporalWorkflowId, @@ -184,7 +184,7 @@ export const initializeFlow = (params: { name, temporalWorkflowId, flowInputs, - flowDefinitionId, + flowDefinitionEntityId, steps: flowDefinition.steps.map((stepDefinition) => stepDefinition.kind === "action" ? initializeActionStep({ flowInputs, stepDefinition }) 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 c988061c631..29b7d5d0d05 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,3 +1,4 @@ +import { splitEntityId } from "@blockprotocol/type-system"; import { type HashEntity, queryEntities } from "@local/hash-graph-sdk/entity"; import { currentTimeInstantTemporalAxes, @@ -7,25 +8,27 @@ import { systemEntityTypes } from "@local/hash-isomorphic-utils/ontology-type-id import type { ActorEntityUuid, - EntityUuid, + EntityId, 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. Its edition current at `atTime`, if given, and otherwise its current edition. + * A `Flow Definition` entity, if the user can view it: e.g. for a run or 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; + flowDefinitionEntityId: EntityId; graphApiClient: GraphApi; userAuthentication: { actorId: ActorEntityUuid }; }): Promise | null> => { - const { atTime, flowDefinitionId, graphApiClient, userAuthentication } = + const { atTime, flowDefinitionEntityId, graphApiClient, userAuthentication } = params; + const [webId, entityUuid] = splitEntityId(flowDefinitionEntityId); + const { entities: [flowDefinitionEntity], } = await queryEntities( @@ -34,7 +37,8 @@ export const getFlowDefinitionEntityById = async (params: { { filter: { all: [ - { equal: [{ path: ["uuid"] }, { parameter: flowDefinitionId }] }, + { equal: [{ path: ["webId"] }, { parameter: webId }] }, + { equal: [{ path: ["uuid"] }, { parameter: entityUuid }] }, { equal: [ { path: ["type", "baseUrl"] }, diff --git a/libs/@local/hash-backend-utils/src/flows/shared/get-flow-run-links.ts b/libs/@local/hash-backend-utils/src/flows/shared/get-flow-run-links.ts index efce2dd8980..bb4e837e3eb 100644 --- a/libs/@local/hash-backend-utils/src/flows/shared/get-flow-run-links.ts +++ b/libs/@local/hash-backend-utils/src/flows/shared/get-flow-run-links.ts @@ -16,7 +16,7 @@ import type { GraphApi } from "@local/hash-graph-client"; * was `Triggered By`, if any. */ export type FlowRunLinks = { - flowDefinitionId: EntityUuid | null; + flowDefinitionEntityId: EntityId | null; flowTriggerId: EntityUuid | null; }; @@ -36,7 +36,7 @@ export const getFlowRunLinks = async (params: { const linksByFlowRunEntityId = new Map( flowRunEntityIds.map((flowRunEntityId) => [ flowRunEntityId, - { flowDefinitionId: null, flowTriggerId: null }, + { flowDefinitionEntityId: null, flowTriggerId: null }, ]), ); @@ -91,36 +91,44 @@ export const getFlowRunLinks = async (params: { continue; } - const rightEntityUuid = extractEntityUuidFromEntityId( - linkData.rightEntityId, - ); - const isOfLinkType = (linkEntityTypeBaseUrl: string) => linkEntity.metadata.entityTypeIds.some((entityTypeId) => entityTypeId.startsWith(linkEntityTypeBaseUrl), ); - const field = isOfLinkType(systemLinkEntityTypes.uses.linkEntityTypeBaseUrl) - ? "flowDefinitionId" + const link = isOfLinkType(systemLinkEntityTypes.uses.linkEntityTypeBaseUrl) + ? { + field: "flowDefinitionEntityId" as const, + linkName: "Uses", + target: linkData.rightEntityId, + } : isOfLinkType(systemLinkEntityTypes.triggeredBy.linkEntityTypeBaseUrl) - ? "flowTriggerId" + ? { + field: "flowTriggerId" as const, + linkName: "Triggered By", + target: extractEntityUuidFromEntityId(linkData.rightEntityId), + } : null; - if (!field) { + if (!link) { continue; } /* A run is created with one link of each kind, so another can only have been added since. The last one read wins. */ - const existingEntityUuid = links[field]; + const existingTarget = links[link.field]; - if (existingEntityUuid && existingEntityUuid !== rightEntityUuid) { + if (existingTarget && existingTarget !== link.target) { // eslint-disable-next-line no-console console.warn( - `Flow run ${linkData.leftEntityId} has more than one ${field === "flowDefinitionId" ? "Uses" : "Triggered By"} link in its web, to ${existingEntityUuid} and ${rightEntityUuid}`, + `Flow run ${linkData.leftEntityId} has more than one ${link.linkName} link in its web, to ${existingTarget} and ${link.target}`, ); } - links[field] = rightEntityUuid; + if (link.field === "flowDefinitionEntityId") { + links.flowDefinitionEntityId = link.target; + } else { + links.flowTriggerId = link.target; + } } return linksByFlowRunEntityId; diff --git a/libs/@local/hash-backend-utils/src/flows/shared/get-hash-web-id.ts b/libs/@local/hash-backend-utils/src/flows/shared/get-hash-web-id.ts new file mode 100644 index 00000000000..ba2d6ba4ded --- /dev/null +++ b/libs/@local/hash-backend-utils/src/flows/shared/get-hash-web-id.ts @@ -0,0 +1,21 @@ +import { extractWebIdFromEntityId } from "@blockprotocol/type-system"; + +import { getHashInstance } from "../../hash-instance.js"; + +import type { ActorEntityUuid, WebId } from "@blockprotocol/type-system"; +import type { GraphApi } from "@local/hash-graph-client"; + +/** + * The id of the `h` web, which HASH's own flow definitions are seeded into: the web the `HASH Instance` entity is in. + */ +export const getHashWebId = async ( + graphApiClient: GraphApi, + authentication: { actorId: ActorEntityUuid }, +): Promise => { + const { entity } = await getHashInstance( + { graphApi: graphApiClient }, + authentication, + ); + + return extractWebIdFromEntityId(entity.metadata.recordId.entityId); +}; 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 index 6a12288f7db..4d381646931 100644 --- a/libs/@local/hash-isomorphic-utils/src/flows/flow-definition-ids.ts +++ b/libs/@local/hash-isomorphic-utils/src/flows/flow-definition-ids.ts @@ -1,7 +1,13 @@ -import type { EntityUuid } from "@blockprotocol/type-system"; +import { + entityIdFromComponents, + extractEntityUuidFromEntityId, +} from "@blockprotocol/type-system"; + +import type { EntityId, EntityUuid, WebId } from "@blockprotocol/type-system"; /** - * The ids of the flows defined in code, which are also the entity uuids of their `Flow Definition` entities. + * The ids of the flows defined in code, which are also the entity uuids of their `Flow Definition` entities in the + * `h` web. * * 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 @@ -21,8 +27,28 @@ export const flowDefinitionIds = { ftseInvestors: "25f32cd2-6f6e-5641-92b7-d9a90e517e0f" as EntityUuid, } as const; +/** + * The entity id of one of the flows defined in code. They're seeded into the `h` web, the web the instance's + * `HASH Instance` entity is in, whose id differs on every instance. + */ +export const generateHashFlowDefinitionEntityId = ( + hashWebId: WebId, + flowDefinitionId: EntityUuid, +): EntityId => entityIdFromComponents(hashWebId, flowDefinitionId); + +/** + * Whether a run or definition is of one of the flows defined in code. Entity uuids are unique across webs, so the + * uuid alone identifies it. + */ +export const isHashFlowDefinitionEntityId = ( + flowDefinitionEntityId: EntityId, + flowDefinitionId: EntityUuid, +) => extractEntityUuidFromEntityId(flowDefinitionEntityId) === flowDefinitionId; + /** * Whether a run or definition is of the goal flow. */ -export const isGoalFlowDefinitionId = (flowDefinitionId: string) => - flowDefinitionId === flowDefinitionIds.goal; +export const isGoalFlowDefinitionEntityId = ( + flowDefinitionEntityId: EntityId, +) => + isHashFlowDefinitionEntityId(flowDefinitionEntityId, flowDefinitionIds.goal); diff --git a/libs/@local/hash-isomorphic-utils/src/flows/frontend-paths.ts b/libs/@local/hash-isomorphic-utils/src/flows/frontend-paths.ts index 64c79d8db31..f0c7131a11c 100644 --- a/libs/@local/hash-isomorphic-utils/src/flows/frontend-paths.ts +++ b/libs/@local/hash-isomorphic-utils/src/flows/frontend-paths.ts @@ -3,13 +3,18 @@ import { type EntityId, } from "@blockprotocol/type-system"; +/** + * The page of a flow definition. The shortname names the definition's web, so the path only needs the uuid of its + * entity. + */ export const generateFlowDefinitionPath = ({ shortname, - flowDefinitionId, + flowDefinitionEntityId, }: { shortname: string; - flowDefinitionId: string; -}) => `/@${shortname}/flows/${flowDefinitionId}`; + flowDefinitionEntityId: EntityId; +}) => + `/@${shortname.replace(/^@/, "")}/flows/${extractEntityUuidFromEntityId(flowDefinitionEntityId)}`; /** * The page of a flow run. The shortname names the run's web, so the path only needs the uuid of its entity. @@ -26,8 +31,8 @@ export const generateWorkerRunPath = ({ export const workerFlowFilterParam = "flow"; export const generatedFilteredWorkersPath = ({ - flowDefinitionIds, + flowDefinitionEntityIds, }: { - flowDefinitionIds: string[]; + flowDefinitionEntityIds: EntityId[]; }) => - `/workers?${flowDefinitionIds.map((flowDefinitionId) => `${workerFlowFilterParam}=${flowDefinitionId}`).join("&")}`; + `/workers?${flowDefinitionEntityIds.map((flowDefinitionEntityId) => `${workerFlowFilterParam}=${encodeURIComponent(flowDefinitionEntityId)}`).join("&")}`; diff --git a/libs/@local/hash-isomorphic-utils/src/flows/schedule-types.ts b/libs/@local/hash-isomorphic-utils/src/flows/schedule-types.ts index 136c15eae5f..ab68cfd08aa 100644 --- a/libs/@local/hash-isomorphic-utils/src/flows/schedule-types.ts +++ b/libs/@local/hash-isomorphic-utils/src/flows/schedule-types.ts @@ -1,6 +1,6 @@ import type { ScheduleOverlapPolicyDataType } from "../system-types/flowschedule.js"; import type { FlowDataSources, FlowInputValues } from "./types.js"; -import type { EntityUuid, WebId } from "@blockprotocol/type-system"; +import type { EntityId, WebId } from "@blockprotocol/type-system"; /** * Interval-based schedule specification. @@ -39,8 +39,8 @@ export const defaultScheduleCatchupWindowMs = 60 * 60 * 1000; export type CreateFlowScheduleInput = { /** Human-readable name for this schedule */ name: string; - /** The uuid of the `Flow Definition` entity the schedule starts runs of */ - flowDefinitionId: EntityUuid; + /** The `Flow Definition` entity the schedule starts runs of */ + flowDefinitionEntityId: EntityId; /** The web this schedule belongs to */ webId: WebId; /** The scheduling specification */ diff --git a/libs/@local/hash-isomorphic-utils/src/flows/temporal-types.ts b/libs/@local/hash-isomorphic-utils/src/flows/temporal-types.ts index a051ce7e801..e730c8e710a 100644 --- a/libs/@local/hash-isomorphic-utils/src/flows/temporal-types.ts +++ b/libs/@local/hash-isomorphic-utils/src/flows/temporal-types.ts @@ -23,10 +23,10 @@ export type BaseRunFlowWorkflowParams = { */ flowRunEntityUuid?: EntityUuid; /** - * The uuid of the `Flow Definition` entity to run. The run loads the definition when it starts, so it runs the - * edition current at that time. + * The `Flow Definition` entity to run. The run loads the definition when it starts, so it runs the edition current + * at that time. */ - flowDefinitionId: EntityUuid; + flowDefinitionEntityId: EntityId; /** * Optional name for the flow run. If not provided, the flow definition name is used. * For scheduled flows, this is typically the schedule name. diff --git a/libs/@local/hash-isomorphic-utils/src/flows/types.ts b/libs/@local/hash-isomorphic-utils/src/flows/types.ts index 859e67f5080..36f766bb544 100644 --- a/libs/@local/hash-isomorphic-utils/src/flows/types.ts +++ b/libs/@local/hash-isomorphic-utils/src/flows/types.ts @@ -716,7 +716,7 @@ export type LocalFlowRun< name: string; temporalWorkflowId: string; flowInputs: FlowInputValues; - flowDefinitionId: EntityUuid; + flowDefinitionEntityId: EntityId; steps: FlowStep[]; outputs?: StepOutput[]; }; diff --git a/libs/@local/hash-isomorphic-utils/src/graphql/queries/flow.queries.ts b/libs/@local/hash-isomorphic-utils/src/graphql/queries/flow.queries.ts index 0808963c798..26ae948c42a 100644 --- a/libs/@local/hash-isomorphic-utils/src/graphql/queries/flow.queries.ts +++ b/libs/@local/hash-isomorphic-utils/src/graphql/queries/flow.queries.ts @@ -7,7 +7,7 @@ export const getFlowRunsQuery = gql` nextCursor flowRuns { name - flowDefinitionId + flowDefinitionEntityId flowRunEntityId flowScheduleId webId @@ -37,7 +37,7 @@ export const getFlowRunById = gql` query getFlowRunById($flowRunEntityId: EntityId!) { getFlowRunById(flowRunEntityId: $flowRunEntityId) { name - flowDefinitionId + flowDefinitionEntityId flowRunEntityId flowScheduleId failureMessage diff --git a/libs/@local/hash-isomorphic-utils/src/graphql/type-defs/knowledge/flow.typedef.ts b/libs/@local/hash-isomorphic-utils/src/graphql/type-defs/knowledge/flow.typedef.ts index bef22808051..ad592e9eede 100644 --- a/libs/@local/hash-isomorphic-utils/src/graphql/type-defs/knowledge/flow.typedef.ts +++ b/libs/@local/hash-isomorphic-utils/src/graphql/type-defs/knowledge/flow.typedef.ts @@ -134,9 +134,9 @@ export const flowTypedef = gql` """ flowScheduleId: EntityUuid """ - The id for the definition of the flow this run is executing (the template for the flow) + The entity id of the Flow Definition entity this run is executing. """ - flowDefinitionId: String! + flowDefinitionEntityId: EntityId! """ A user-facing name for the flow run to distinguish it from other runs of its kind, which might include a key input or a summary of its key inputs. @@ -208,9 +208,9 @@ export const flowTypedef = gql` extend type Query { getFlowRuns( """ - Return only flow runs that are based off specific definitions + Return only flow runs of these Flow Definition entities """ - flowDefinitionIds: [String!] + flowDefinitionEntityIds: [EntityId!] """ Return only flows that match the given status """ @@ -236,7 +236,7 @@ export const flowTypedef = gql` """ startFlow( dataSources: FlowDataSources - flowDefinitionId: EntityUuid! + flowDefinitionEntityId: EntityId! flowInputs: FlowInputValues! webId: WebId! ): EntityId! From 4724f764012ef8c400c3ec3290e004b06432cc5b Mon Sep 17 00:00:00 2001 From: Ciaran Morinan <37743469+CiaranMn@users.noreply.github.com> Date: Mon, 19 Oct 2026 14:04:50 +0100 Subject: [PATCH 09/13] Drop the claim that a flow run's uuid is its workflow id, which isn't so for scheduled runs --- .../src/graphql/type-defs/knowledge/flow.typedef.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/@local/hash-isomorphic-utils/src/graphql/type-defs/knowledge/flow.typedef.ts b/libs/@local/hash-isomorphic-utils/src/graphql/type-defs/knowledge/flow.typedef.ts index bef22808051..1a713ae4c00 100644 --- a/libs/@local/hash-isomorphic-utils/src/graphql/type-defs/knowledge/flow.typedef.ts +++ b/libs/@local/hash-isomorphic-utils/src/graphql/type-defs/knowledge/flow.typedef.ts @@ -125,7 +125,7 @@ export const flowTypedef = gql` type FlowRun { """ - The entity id of the run's Flow Run entity. Its uuid is the id of the run's Temporal workflow. + The entity id of the run's Flow Run entity. """ flowRunEntityId: EntityId! """ From b340e1268143bcb865d4814296768ac9595b8dc4 Mon Sep 17 00:00:00 2001 From: Ciaran Morinan <37743469+CiaranMn@users.noreply.github.com> Date: Mon, 19 Oct 2026 14:06:39 +0100 Subject: [PATCH 10/13] Link an edition's flow run under the run's own web, which may not be the entity's --- .../entity/entity-editor/history-section.tsx | 11 +---------- .../history-section/history-table.tsx | 14 ++------------ .../history-table/provenance.tsx | 17 +++++++++++------ 3 files changed, 14 insertions(+), 28 deletions(-) diff --git a/apps/hash-frontend/src/pages/shared/entity/entity-editor/history-section.tsx b/apps/hash-frontend/src/pages/shared/entity/entity-editor/history-section.tsx index fb6958bdb13..4ead9746423 100644 --- a/apps/hash-frontend/src/pages/shared/entity/entity-editor/history-section.tsx +++ b/apps/hash-frontend/src/pages/shared/entity/entity-editor/history-section.tsx @@ -9,7 +9,6 @@ import { fullDecisionTimeAxis, } from "@local/hash-isomorphic-utils/graph-queries"; -import { useUserOrOrgShortnameByWebId } from "../../../../components/hooks/use-user-or-org-shortname-by-owned-by-id"; import { getEntityDiffsQuery, queryEntitySubgraphQuery, @@ -31,10 +30,6 @@ import type { DiffEntityInput } from "@local/hash-graph-sdk/entity"; export const HistorySection = ({ entityId }: { entityId: EntityId }) => { const [webId, entityUuid, _draftUuid] = splitEntityId(entityId); - const { shortname } = useUserOrOrgShortnameByWebId({ - webId, - }); - const { data: editionsData, loading: editionsLoading } = useQuery< QueryEntitySubgraphQuery, QueryEntitySubgraphQueryVariables @@ -152,11 +147,7 @@ export const HistorySection = ({ entityId }: { entityId: EntityId }) => { {loading || !subgraph ? ( ) : ( - + )} diff --git a/apps/hash-frontend/src/pages/shared/entity/entity-editor/history-section/history-table.tsx b/apps/hash-frontend/src/pages/shared/entity/entity-editor/history-section/history-table.tsx index 2aa4262d73e..3d88aa26958 100644 --- a/apps/hash-frontend/src/pages/shared/entity/entity-editor/history-section/history-table.tsx +++ b/apps/hash-frontend/src/pages/shared/entity/entity-editor/history-section/history-table.tsx @@ -96,7 +96,6 @@ const TableRow = memo( isLastRow, numberColumnWidth, scrollContainerRef, - shortname, subgraph, }: HistoryRowData) => { const { number, timestamp } = event; @@ -243,11 +242,7 @@ const TableRow = memo( - + @@ -261,7 +256,6 @@ type HistoryRowData = { isLastRow: boolean; numberColumnWidth: number; scrollContainerRef: React.RefObject; - shortname: string; subgraph: Subgraph; }; @@ -275,18 +269,15 @@ const createRowContent: CreateVirtualizedRowContentFn = ( isFirstRow={row.data.isFirstRow} isLastRow={row.data.isLastRow} scrollContainerRef={row.data.scrollContainerRef} - shortname={row.data.shortname} subgraph={row.data.subgraph} /> ); export const HistoryTable = ({ events, - shortname, subgraph, }: { events: HistoryEvent[]; - shortname: string; subgraph: Subgraph; }) => { const [sort, setSort] = useState>({ @@ -325,11 +316,10 @@ export const HistoryTable = ({ isLastRow: index === events.length - 1, numberColumnWidth, scrollContainerRef, - shortname, subgraph, }, })); - }, [events, sort, subgraph, scrollContainerRef, shortname]); + }, [events, sort, subgraph, scrollContainerRef]); const columns = useMemo(() => createColumns(rows.length), [rows]); diff --git a/apps/hash-frontend/src/pages/shared/entity/entity-editor/history-section/history-table/provenance.tsx b/apps/hash-frontend/src/pages/shared/entity/entity-editor/history-section/history-table/provenance.tsx index 44538377d20..cc69f3ed650 100644 --- a/apps/hash-frontend/src/pages/shared/entity/entity-editor/history-section/history-table/provenance.tsx +++ b/apps/hash-frontend/src/pages/shared/entity/entity-editor/history-section/history-table/provenance.tsx @@ -1,6 +1,7 @@ import { Box, Stack, Typography } from "@mui/material"; import { useState } from "react"; +import { extractWebIdFromEntityId } from "@blockprotocol/type-system"; import { ArrowUpRightRegularIcon, CodeIcon, @@ -10,6 +11,7 @@ import { } from "@hashintel/design-system"; import { generateWorkerRunPath } from "@local/hash-isomorphic-utils/flows/frontend-paths"; +import { useUserOrOrgShortnameByWebId } from "../../../../../../components/hooks/use-user-or-org-shortname-by-owned-by-id"; import { SearchIcon } from "../../../../../../shared/icons/search-icon"; import { UserIcon } from "../../../../../../shared/icons/user-icon"; import { Link } from "../../../../../../shared/ui/link"; @@ -58,17 +60,22 @@ const ProvenanceRow = ({ children }: PropsWithChildren) => ( export const Provenance = ({ event, - shortname, subgraph, }: { event: HistoryEvent; - shortname: string; subgraph: Subgraph; }) => { const { provenance: { edition }, } = event; + const flowRunEntityId = edition.origin.id as EntityId | undefined; + + /* The run page is under the shortname of the run's web, which may not be the entity's. */ + const { shortname: flowRunShortname } = useUserOrOrgShortnameByWebId({ + webId: flowRunEntityId ? extractWebIdFromEntityId(flowRunEntityId) : null, + }); + const { actors, loading } = useActors({ accountIds: [edition.createdById], }); @@ -103,8 +110,6 @@ export const Provenance = ({ const OriginIcon = provenanceIconMap[originType]; - const flowRunEntityId = edition.origin.id as EntityId | undefined; - const sources = event.type === "property-update" ? event.provenance.property?.sources @@ -140,11 +145,11 @@ export const Provenance = ({ {originType.split("-").join(" ")} - {flowRunEntityId && ( + {flowRunEntityId && flowRunShortname && ( Date: Mon, 19 Oct 2026 14:27:48 +0100 Subject: [PATCH 11/13] Only link an edition to a flow run when its origin is a flow, and its id an entity id --- .../history-section/history-table/provenance.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/hash-frontend/src/pages/shared/entity/entity-editor/history-section/history-table/provenance.tsx b/apps/hash-frontend/src/pages/shared/entity/entity-editor/history-section/history-table/provenance.tsx index cc69f3ed650..93ac906b380 100644 --- a/apps/hash-frontend/src/pages/shared/entity/entity-editor/history-section/history-table/provenance.tsx +++ b/apps/hash-frontend/src/pages/shared/entity/entity-editor/history-section/history-table/provenance.tsx @@ -1,7 +1,10 @@ import { Box, Stack, Typography } from "@mui/material"; import { useState } from "react"; -import { extractWebIdFromEntityId } from "@blockprotocol/type-system"; +import { + extractWebIdFromEntityId, + isEntityId, +} from "@blockprotocol/type-system"; import { ArrowUpRightRegularIcon, CodeIcon, @@ -20,7 +23,7 @@ import { SourcesSlideover } from "./provenance/sources-slideover"; import type { HistoryEvent } from "../shared/types"; import type { Subgraph } from "@blockprotocol/graph"; -import type { EntityId, OriginProvenance } from "@blockprotocol/type-system"; +import type { OriginProvenance } from "@blockprotocol/type-system"; import type { SvgIconProps, SxProps, Theme } from "@mui/material"; import type { FunctionComponent, PropsWithChildren } from "react"; @@ -69,7 +72,13 @@ export const Provenance = ({ provenance: { edition }, } = event; - const flowRunEntityId = edition.origin.id as EntityId | undefined; + /* A flow's origin id is the entity id of the run that made the edition. Other origins give ids of their own. */ + const flowRunEntityId = + edition.origin.type === "flow" && + edition.origin.id !== undefined && + isEntityId(edition.origin.id) + ? edition.origin.id + : undefined; /* The run page is under the shortname of the run's web, which may not be the entity's. */ const { shortname: flowRunShortname } = useUserOrOrgShortnameByWebId({ From 6c6a914f87faffec712f6e69137076df7d2dcad7 Mon Sep 17 00:00:00 2001 From: Ciaran Morinan <37743469+CiaranMn@users.noreply.github.com> Date: Mon, 19 Oct 2026 14:47:08 +0100 Subject: [PATCH 12/13] Show a for-each step's open input requests on its node, which are raised by its runs per item --- .../shared/flow-visualizer/swimlane/custom-node.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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") { From 441fd8b2507c78297cd66d63fee82f707e364685 Mon Sep 17 00:00:00 2001 From: Ciaran Morinan <37743469+CiaranMn@users.noreply.github.com> Date: Tue, 20 Oct 2026 08:16:55 +0100 Subject: [PATCH 13/13] Pass flow definition entity ids in the flow scenario runner --- .../flow-scenarios/run-flow-scenario.ts | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/apps/hash-ai-worker-ts/scripts/flow-scenarios/run-flow-scenario.ts b/apps/hash-ai-worker-ts/scripts/flow-scenarios/run-flow-scenario.ts index fb4fadd1b0c..3da8ca5296e 100644 --- a/apps/hash-ai-worker-ts/scripts/flow-scenarios/run-flow-scenario.ts +++ b/apps/hash-ai-worker-ts/scripts/flow-scenarios/run-flow-scenario.ts @@ -294,7 +294,7 @@ const getUserFlowDefinitionId = ( ) => uuidv5(userId, scenarioFlowDefinitionId) as EntityUuid; /** - * Runs load their definition from the `Flow Definition` entity whose uuid is the definition's id. Migrations only + * Runs load their definition from its `Flow Definition` entity, in the user's web here. Migrations only * seed the flows HASH provides, so the scenario's definition is written to the user's web: created if it doesn't * exist, and updated if it differs, like `seedFlowDefinitions` does for seeded flows. */ @@ -318,7 +318,10 @@ const ensureFlowDefinitionEntity = async ({ const properties = mapFlowDefinitionToEntityProperties(flowDefinition); const existingEntity = await getFlowDefinitionEntityById({ - flowDefinitionId, + flowDefinitionEntityId: entityIdFromComponents( + userId as WebId, + flowDefinitionId, + ), graphApiClient: graphApi, userAuthentication: authentication, }); @@ -446,6 +449,10 @@ const startRun = async ({ }) => { const workflowId = generateUuid() as EntityUuid; const webId = userId as WebId; + const flowDefinitionEntityId = entityIdFromComponents( + webId, + flow.flowDefinitionId, + ); const params: RunFlowWorkflowParams = { ...(taskQueue === "ai" ? { @@ -460,7 +467,7 @@ const startRun = async ({ : {}), flowRunEntityUuid: workflowId, flowInputs: inputs, - flowDefinitionId: flow.flowDefinitionId, + flowDefinitionEntityId, userAuthentication: { actorId: userId }, webId, }; @@ -469,7 +476,7 @@ const startRun = async ({ taskQueue, args: [params], memo: { - flowDefinitionId: flow.flowDefinitionId, + flowDefinitionEntityId, userAccountId: userId, webId, }, @@ -902,7 +909,10 @@ const main = async () => { const workflowError = await workflowErrorWhenClosed; const flowRun = await getFlowRunFromTemporalWorkflowId({ - flowDefinitionId, + flowDefinitionEntityId: entityIdFromComponents( + userId as WebId, + flowDefinitionId, + ), flowRunEntityId: entityIdFromComponents(userId as WebId, workflowId), name: flowDefinition.name, temporalClient: client,