diff --git a/src/components/Editor/Context/PipelineDetails.tsx b/src/components/Editor/Context/PipelineDetails.tsx
index 10205fff14..453bcee2ea 100644
--- a/src/components/Editor/Context/PipelineDetails.tsx
+++ b/src/components/Editor/Context/PipelineDetails.tsx
@@ -10,7 +10,6 @@ import { CopyText } from "@/components/shared/CopyText/CopyText";
import { FavoriteToggle } from "@/components/shared/FavoriteToggle";
import { PipelineDescription } from "@/components/shared/PipelineDescription/PipelineDescription";
import { PipelineRunNameTemplateEditor } from "@/components/shared/PipelineRunNameTemplate/PipelineRunNameTemplateEditor";
-import { useFlagValue } from "@/components/shared/Settings/useFlags";
import { BlockStack } from "@/components/ui/layout";
import useToastNotification from "@/hooks/useToastNotification";
import { useComponentSpec } from "@/providers/ComponentSpecProvider";
@@ -29,10 +28,6 @@ const PipelineDetails = () => {
const notify = useToastNotification();
const { componentSpec, digest, globalValidationIssues } = useComponentSpec();
- const templatizedRunNameEnabled = useFlagValue(
- "templatized-pipeline-run-name",
- );
-
const { handleIssueClick, groupedIssues } = useValidationIssueNavigation(
globalValidationIssues,
);
@@ -127,11 +122,9 @@ const PipelineDetails = () => {
- {templatizedRunNameEnabled && (
-
-
-
- )}
+
+
+
{digest && (
}`, `${date.timestamp}`, `${date.short}`, `${date.long}`, and `${annotations.}`."
+ "body": "Set a Run name template in the pipeline's configuration panel and every submitted run gets its own auto-generated name, while the pipeline keeps its own. Available placeholders: `${arguments.}`, `${date.timestamp}`, `${date.short}`, `${date.long}`, and `${annotations.}`."
},
{
"id": "submit-with-arguments",
diff --git a/src/components/PipelineRun/components/RerunPipelineButton.tsx b/src/components/PipelineRun/components/RerunPipelineButton.tsx
index 97c883d4db..87bd653533 100644
--- a/src/components/PipelineRun/components/RerunPipelineButton.tsx
+++ b/src/components/PipelineRun/components/RerunPipelineButton.tsx
@@ -7,7 +7,6 @@ import { useAuthLocalStorage } from "@/components/shared/Authentication/useAuthL
import { useAwaitAuthorization } from "@/components/shared/Authentication/useAwaitAuthorization";
import TooltipButton from "@/components/shared/Buttons/TooltipButton";
import { buildTaskSpecShape } from "@/components/shared/PipelineRunNameTemplate/types";
-import { useFlagValue } from "@/components/shared/Settings/useFlags";
import { Icon } from "@/components/ui/icon";
import useToastNotification from "@/hooks/useToastNotification";
import { useBackend } from "@/providers/BackendProvider";
@@ -35,7 +34,6 @@ export const RerunPipelineButton = ({
showTooltip = true,
...rest
}: RerunPipelineButtonProps) => {
- const runNameOverride = useFlagValue("templatized-pipeline-run-name");
const { backendUrl } = useBackend();
const navigate = useNavigate();
const notify = useToastNotification();
@@ -85,7 +83,6 @@ export const RerunPipelineButton = ({
taskArguments: executionData?.rootDetails?.task_spec
.arguments as Record,
authorizationToken,
- runNameOverride,
onSuccess: resolve,
onError: reject,
});
diff --git a/src/components/shared/Submitters/Tangle/TangleSubmitter.tsx b/src/components/shared/Submitters/Tangle/TangleSubmitter.tsx
index f9081b063b..0dc7c3e53d 100644
--- a/src/components/shared/Submitters/Tangle/TangleSubmitter.tsx
+++ b/src/components/shared/Submitters/Tangle/TangleSubmitter.tsx
@@ -43,7 +43,6 @@ interface TangleSubmitterProps {
}
function useSubmitPipeline() {
- const runNameOverride = useFlagValue("templatized-pipeline-run-name");
const { awaitAuthorization, isAuthorized } = useAwaitAuthorization();
const queryClient = useQueryClient();
const { getToken } = useAuthLocalStorage();
@@ -76,7 +75,6 @@ function useSubmitPipeline() {
submitPipelineRun(componentSpec, backendUrl, {
authorizationToken: authorizationToken.current,
taskArguments,
- runNameOverride,
onSuccess: (data) => {
resolve(data);
onSuccess(data);
diff --git a/src/flags.ts b/src/flags.ts
index 2d7a932197..3f1d7749ff 100644
--- a/src/flags.ts
+++ b/src/flags.ts
@@ -31,14 +31,6 @@ export const ExistingFlags: ConfigFlags = {
category: "setting",
},
- ["templatized-pipeline-run-name"]: {
- name: "Templatized pipeline run name",
- description:
- "Enable the templatized pipeline run name feature. This will generate a run name for each pipeline run based on the template with placeholders.",
- default: false,
- category: "beta",
- },
-
["input-aggregator"]: {
name: "Input Aggregator Component",
description:
diff --git a/src/models/componentSpec/annotations.ts b/src/models/componentSpec/annotations.ts
index b57929af13..de2a25bddb 100644
--- a/src/models/componentSpec/annotations.ts
+++ b/src/models/componentSpec/annotations.ts
@@ -38,6 +38,7 @@ interface AnnotationTypeMap {
"flex-nodes": FlexNodeData[];
notes: string;
tags: string[];
+ "run-name-template": string;
}
type KnownAnnotationKey = keyof AnnotationTypeMap;
@@ -125,6 +126,11 @@ const codecs = {
: [],
defaultValue: [] as string[],
},
+ "run-name-template": {
+ serialize: (value: string) => value,
+ deserialize: (raw: unknown) => (typeof raw === "string" ? raw : ""),
+ defaultValue: "",
+ },
} satisfies {
[K in KnownAnnotationKey]: AnnotationCodec;
};
diff --git a/src/routes/v2/pages/Editor/components/PipelineDetailsContent/PipelineDetailsContent.tsx b/src/routes/v2/pages/Editor/components/PipelineDetailsContent/PipelineDetailsContent.tsx
index 0ac6e37049..1c3a844c17 100644
--- a/src/routes/v2/pages/Editor/components/PipelineDetailsContent/PipelineDetailsContent.tsx
+++ b/src/routes/v2/pages/Editor/components/PipelineDetailsContent/PipelineDetailsContent.tsx
@@ -16,6 +16,7 @@ import { useSpec } from "@/routes/v2/shared/providers/SpecContext";
import { useSharedStores } from "@/routes/v2/shared/store/SharedStoreContext";
import {
PIPELINE_NOTES_ANNOTATION,
+ RUN_NAME_TEMPLATE_ANNOTATION,
SYSTEM_ANNOTATIONS,
} from "@/utils/annotations";
@@ -32,8 +33,12 @@ export const PipelineDetailsContent = observer(
const { navigation } = useSharedStores();
const pipelineSpec = useSpec();
const notify = useToastNotification();
- const { updatePipelineDescription, updatePipelineNotes, renameSubgraph } =
- usePipelineActions();
+ const {
+ updatePipelineDescription,
+ updatePipelineNotes,
+ updateRunNameTemplate,
+ renameSubgraph,
+ } = usePipelineActions();
if (!pipelineSpec) {
return (
@@ -69,6 +74,17 @@ export const PipelineDetailsContent = observer(
}
};
+ const handleRunNameTemplateCommit = (value: string | undefined) => {
+ const currentTemplate =
+ pipelineSpec.annotations.get(RUN_NAME_TEMPLATE_ANNOTATION) || undefined;
+ if (value !== currentTemplate) {
+ updateRunNameTemplate(pipelineSpec, value);
+ track(
+ "v2.pipeline_editor.configuration_panel.run_name_template.updated",
+ );
+ }
+ };
+
const handleRenameSubgraph = (newName: string): boolean => {
const success = renameSubgraph(newName);
if (success) {
@@ -123,6 +139,17 @@ export const PipelineDetailsContent = observer(
testId="pipeline-notes-input"
/>
+
+
diff --git a/src/routes/v2/pages/Editor/store/actions/__tests__/pipeline.actions.test.ts b/src/routes/v2/pages/Editor/store/actions/__tests__/pipeline.actions.test.ts
new file mode 100644
index 0000000000..1182d41bda
--- /dev/null
+++ b/src/routes/v2/pages/Editor/store/actions/__tests__/pipeline.actions.test.ts
@@ -0,0 +1,38 @@
+import { describe, expect, it } from "vitest";
+
+import { ComponentSpec, serializeComponentSpec } from "@/models/componentSpec";
+import { updateRunNameTemplate } from "@/routes/v2/pages/Editor/store/actions/pipeline.actions";
+import { RUN_NAME_TEMPLATE_ANNOTATION } from "@/utils/annotations";
+
+const noopUndo = {
+ withGroup: (_label: string, fn: () => T): T => fn(),
+};
+
+const makeSpec = () => new ComponentSpec({ $id: "spec_1", name: "Pipeline" });
+
+describe("updateRunNameTemplate", () => {
+ it("stores the template so it survives serialization to the wire spec", () => {
+ const spec = makeSpec();
+
+ updateRunNameTemplate(noopUndo, spec, "${arguments.dataset}-run");
+
+ expect(spec.annotations.get(RUN_NAME_TEMPLATE_ANNOTATION)).toBe(
+ "${arguments.dataset}-run",
+ );
+ expect(
+ serializeComponentSpec(spec).metadata?.annotations?.[
+ RUN_NAME_TEMPLATE_ANNOTATION
+ ],
+ ).toBe("${arguments.dataset}-run");
+ });
+
+ it("removes the annotation when the template is cleared", () => {
+ const spec = makeSpec();
+ updateRunNameTemplate(noopUndo, spec, "${date.timestamp}");
+
+ updateRunNameTemplate(noopUndo, spec, undefined);
+
+ expect(spec.annotations.has(RUN_NAME_TEMPLATE_ANNOTATION)).toBe(false);
+ expect(spec.annotations.get(RUN_NAME_TEMPLATE_ANNOTATION)).toBe("");
+ });
+});
diff --git a/src/routes/v2/pages/Editor/store/actions/pipeline.actions.ts b/src/routes/v2/pages/Editor/store/actions/pipeline.actions.ts
index d4710e671c..4bb945751a 100644
--- a/src/routes/v2/pages/Editor/store/actions/pipeline.actions.ts
+++ b/src/routes/v2/pages/Editor/store/actions/pipeline.actions.ts
@@ -12,6 +12,7 @@ import {
EDITOR_POSITION_ANNOTATION,
PIPELINE_NOTES_ANNOTATION,
PIPELINE_TAGS_ANNOTATION,
+ RUN_NAME_TEMPLATE_ANNOTATION,
} from "@/utils/annotations";
import { idGen } from "./utils";
@@ -63,6 +64,21 @@ export function updatePipelineNotes(
});
}
+export function updateRunNameTemplate(
+ undo: UndoGroupable,
+ spec: ComponentSpec,
+ template: string | undefined,
+): boolean {
+ return undo.withGroup("Update run name template", () => {
+ if (template) {
+ spec.annotations.set(RUN_NAME_TEMPLATE_ANNOTATION, template);
+ } else {
+ spec.annotations.remove(RUN_NAME_TEMPLATE_ANNOTATION);
+ }
+ return true;
+ });
+}
+
export function updatePipelineTags(
undo: UndoGroupable,
spec: ComponentSpec,
diff --git a/src/routes/v2/pages/Editor/store/actions/usePipelineActions.ts b/src/routes/v2/pages/Editor/store/actions/usePipelineActions.ts
index a24ce7cf4c..f93180cee6 100644
--- a/src/routes/v2/pages/Editor/store/actions/usePipelineActions.ts
+++ b/src/routes/v2/pages/Editor/store/actions/usePipelineActions.ts
@@ -8,6 +8,7 @@ import {
updatePipelineDescription,
updatePipelineNotes,
updatePipelineTags,
+ updateRunNameTemplate,
} from "./pipeline.actions";
export function usePipelineActions() {
@@ -20,6 +21,7 @@ export function usePipelineActions() {
updatePipelineDescription: updatePipelineDescription.bind(null, undo),
updatePipelineNotes: updatePipelineNotes.bind(null, undo),
updatePipelineTags: updatePipelineTags.bind(null, undo),
+ updateRunNameTemplate: updateRunNameTemplate.bind(null, undo),
createSubgraph: createSubgraph.bind(null, undo),
};
}
diff --git a/src/routes/v2/pages/RunView/hooks/useRerunPipelineRun.ts b/src/routes/v2/pages/RunView/hooks/useRerunPipelineRun.ts
index 3c624080f3..9ce63e3bc8 100644
--- a/src/routes/v2/pages/RunView/hooks/useRerunPipelineRun.ts
+++ b/src/routes/v2/pages/RunView/hooks/useRerunPipelineRun.ts
@@ -5,7 +5,6 @@ import { isAuthorizationRequired } from "@/components/shared/Authentication/help
import { useAuthLocalStorage } from "@/components/shared/Authentication/useAuthLocalStorage";
import { useAwaitAuthorization } from "@/components/shared/Authentication/useAwaitAuthorization";
import { buildTaskSpecShape } from "@/components/shared/PipelineRunNameTemplate/types";
-import { useFlagValue } from "@/components/shared/Settings/useFlags";
import useToastNotification from "@/hooks/useToastNotification";
import { useBackend } from "@/providers/BackendProvider";
import { useExecutionData } from "@/providers/ExecutionDataProvider";
@@ -25,7 +24,6 @@ export function useRerunPipelineRun(componentSpec?: ComponentSpec) {
const navigate = useNavigate();
const notify = useToastNotification();
const { backendUrl } = useBackend();
- const runNameOverride = useFlagValue("templatized-pipeline-run-name");
const { awaitAuthorization, isAuthorized } = useAwaitAuthorization();
const { getToken } = useAuthLocalStorage();
const { rootDetails } = useExecutionData();
@@ -52,7 +50,6 @@ export function useRerunPipelineRun(componentSpec?: ComponentSpec) {
canonicalName,
taskArguments,
authorizationToken,
- runNameOverride,
onSuccess: resolve,
onError: reject,
});
diff --git a/src/utils/annotations.ts b/src/utils/annotations.ts
index ab427e1fd1..89f4b4782e 100644
--- a/src/utils/annotations.ts
+++ b/src/utils/annotations.ts
@@ -11,6 +11,7 @@ import {
FLEX_NODES_ANNOTATION,
PIPELINE_NOTES_ANNOTATION,
PIPELINE_TAGS_ANNOTATION,
+ RUN_NAME_TEMPLATE_ANNOTATION,
SDK_ANNOTATION,
TASK_COLOR_ANNOTATION,
TASK_DISPLAY_NAME_ANNOTATION,
@@ -26,6 +27,7 @@ const PIPELINE_AGGREGATOR_ANNOTATION = "is_input_aggregator";
export const SYSTEM_ANNOTATIONS = [
PIPELINE_NOTES_ANNOTATION,
PIPELINE_TAGS_ANNOTATION,
+ RUN_NAME_TEMPLATE_ANNOTATION,
FLEX_NODES_ANNOTATION,
SDK_ANNOTATION,
EDITOR_POSITION_ANNOTATION,
diff --git a/src/utils/submitPipeline.test.ts b/src/utils/submitPipeline.test.ts
index 4561a8635d..0ad1af19b2 100644
--- a/src/utils/submitPipeline.test.ts
+++ b/src/utils/submitPipeline.test.ts
@@ -235,6 +235,71 @@ describe("submitPipelineRun", () => {
});
});
+ describe("run name template", () => {
+ const templatedSpec = (template: string): ComponentSpec => ({
+ name: "my-pipeline",
+ inputs: [{ name: "dataset", default: "iris" }],
+ implementation: { container: { image: "test:latest" } },
+ metadata: { annotations: { "run-name-template": template } },
+ });
+
+ it("resolves the template into the submitted run name", async () => {
+ await submitPipelineRun(
+ templatedSpec("${arguments.dataset}-run"),
+ mockBackendUrl,
+ );
+
+ const [payload] = vi.mocked(pipelineRunService.createPipelineRun).mock
+ .calls[0]!;
+ expect(payload.root_task.componentRef.spec!.name).toBe("iris-run");
+ });
+
+ it("preserves the pipeline name as the canonical name annotation", async () => {
+ await submitPipelineRun(
+ templatedSpec("${arguments.dataset}-run"),
+ mockBackendUrl,
+ );
+
+ const [payload] = vi.mocked(pipelineRunService.createPipelineRun).mock
+ .calls[0]!;
+ expect(payload.root_task.annotations).toEqual({
+ "canonical-pipeline-name": "my-pipeline",
+ });
+ expect(pipelineRunService.savePipelineRun).toHaveBeenCalledWith(
+ mockPipelineRun,
+ "my-pipeline",
+ undefined,
+ "iris-run",
+ );
+ });
+
+ it("prefers taskArguments over input defaults when resolving placeholders", async () => {
+ await submitPipelineRun(
+ templatedSpec("${arguments.dataset}-run"),
+ mockBackendUrl,
+ { taskArguments: { dataset: "titanic" } },
+ );
+
+ const [payload] = vi.mocked(pipelineRunService.createPipelineRun).mock
+ .calls[0]!;
+ expect(payload.root_task.componentRef.spec!.name).toBe("titanic-run");
+ });
+
+ it("leaves the pipeline name untouched when there is no template", async () => {
+ const componentSpec: ComponentSpec = {
+ name: "my-pipeline",
+ implementation: { container: { image: "test:latest" } },
+ };
+
+ await submitPipelineRun(componentSpec, mockBackendUrl);
+
+ const [payload] = vi.mocked(pipelineRunService.createPipelineRun).mock
+ .calls[0]!;
+ expect(payload.root_task.componentRef.spec!.name).toBe("my-pipeline");
+ expect(payload.root_task.annotations).toEqual({});
+ });
+ });
+
describe("taskArguments handling", () => {
it("should include taskArguments in payload when provided", async () => {
// Arrange
diff --git a/src/utils/submitPipeline.ts b/src/utils/submitPipeline.ts
index 08c88efcbc..cf95bacadc 100644
--- a/src/utils/submitPipeline.ts
+++ b/src/utils/submitPipeline.ts
@@ -28,7 +28,6 @@ export async function submitPipelineRun(
options?: {
taskArguments?: Record;
authorizationToken?: string;
- runNameOverride?: boolean;
canonicalName?: string;
onSuccess?: (data: PipelineRun) => void;
onError?: (error: Error) => void;
@@ -75,16 +74,15 @@ export async function submitPipelineRun(
.map(([k, v]) => [k, v as string]),
);
- const runNameOverride = options?.runNameOverride
- ? processTemplate(getRunNameTemplate(transformedSpec) ?? "", {
- componentRef: {
- spec: transformedSpec,
- },
- arguments: stringArguments,
- }) || undefined
- : undefined;
+ const templatizedRunName =
+ processTemplate(getRunNameTemplate(transformedSpec) ?? "", {
+ componentRef: {
+ spec: transformedSpec,
+ },
+ arguments: stringArguments,
+ }) || undefined;
- const taskAnnotations = runNameOverride
+ const taskAnnotations = templatizedRunName
? buildAnnotationsWithCanonicalName(pipelineName)
: {};
@@ -96,7 +94,7 @@ export async function submitPipelineRun(
componentRef: {
spec: {
...transformedSpec,
- name: runNameOverride ?? pipelineName,
+ name: templatizedRunName ?? pipelineName,
} as ComponentSpecInput,
},
...(payloadArguments ? { arguments: payloadArguments } : {}),
@@ -115,7 +113,7 @@ export async function submitPipelineRun(
responseData,
pipelineName,
componentSpec.metadata?.annotations?.digest as string | undefined,
- runNameOverride,
+ templatizedRunName,
);
}
options?.onSuccess?.(responseData);