From 81ffa814d9b90b7a6c3770e6835783ef0c701431 Mon Sep 17 00:00:00 2001
From: Krishna Mohan
Date: Tue, 27 Jan 2026 17:06:17 +0530
Subject: [PATCH 1/5] feat: added the feature write the name of artifact with
custom and dynamic inputs
Signed-off-by: Krishna Mohan
---
.../src/components/workflow/ConfigPanel.tsx | 42 +++++--
.../workflow/DynamicArtifactNameInput.tsx | 117 ++++++++++++++++++
.../core/__tests__/artifact-writer.test.ts | 102 ++++++++++++++-
worker/src/components/core/artifact-writer.ts | 88 +++++++++++--
4 files changed, 322 insertions(+), 27 deletions(-)
create mode 100644 frontend/src/components/workflow/DynamicArtifactNameInput.tsx
diff --git a/frontend/src/components/workflow/ConfigPanel.tsx b/frontend/src/components/workflow/ConfigPanel.tsx
index dbdc174f..ca6fd0f8 100644
--- a/frontend/src/components/workflow/ConfigPanel.tsx
+++ b/frontend/src/components/workflow/ConfigPanel.tsx
@@ -31,6 +31,7 @@ import { useComponentStore } from '@/store/componentStore';
import { ParameterFieldWrapper } from './ParameterField';
import { WebhookDetails } from './WebhookDetails';
import { SecretSelect } from '@/components/inputs/SecretSelect';
+import { DynamicArtifactNameInput } from './DynamicArtifactNameInput';
import type { Node } from 'reactflow';
import type { FrontendNodeData } from '@/schemas/node';
import type { ComponentType, KeyboardEvent } from 'react';
@@ -913,6 +914,20 @@ export function ConfigPanel({
placeholder={manualPlaceholder}
onChange={(value) => handleInputOverrideChange(input.id, value)}
/>
+ ) : component?.id === 'core.artifact.writer' &&
+ input.id === 'artifactName' ? (
+ {
+ if (!value || value === '') {
+ handleInputOverrideChange(input.id, undefined);
+ } else {
+ handleInputOverrideChange(input.id, value);
+ }
+ }}
+ disabled={manualLocked}
+ placeholder="{{run_id}}-{{timestamp}}"
+ />
) : (
)}
- {manualLocked ? (
-
- Disconnect the port to edit manual input.
-
- ) : (
-
- {isBooleanInput
- ? 'Select a value or clear manual input to require a port connection.'
- : isListOfTextInput
- ? 'Add entries or clear manual input to require a port connection.'
- : 'Leave blank to require a port connection.'}
-
+ {/* Skip helper text for DynamicArtifactNameInput as it has its own */}
+ {!(component?.id === 'core.artifact.writer' && input.id === 'artifactName') && (
+ manualLocked ? (
+
+ Disconnect the port to edit manual input.
+
+ ) : (
+
+ {isBooleanInput
+ ? 'Select a value or clear manual input to require a port connection.'
+ : isListOfTextInput
+ ? 'Add entries or clear manual input to require a port connection.'
+ : 'Leave blank to require a port connection.'}
+
+ )
)}
)}
diff --git a/frontend/src/components/workflow/DynamicArtifactNameInput.tsx b/frontend/src/components/workflow/DynamicArtifactNameInput.tsx
new file mode 100644
index 00000000..d179fc64
--- /dev/null
+++ b/frontend/src/components/workflow/DynamicArtifactNameInput.tsx
@@ -0,0 +1,117 @@
+import { useState } from 'react';
+import { ChevronDown, ChevronRight, Plus } from 'lucide-react';
+import { Input } from '@/components/ui/input';
+import { cn } from '@/lib/utils';
+
+interface DynamicParameter {
+ placeholder: string;
+ description: string;
+}
+
+const DYNAMIC_PARAMETERS: DynamicParameter[] = [
+ { placeholder: '{{timestamp}}', description: 'Current timestamp' },
+ { placeholder: '{{run_id}}', description: 'Run ID' },
+ { placeholder: '{{dataset}}', description: 'Dataset name' },
+ { placeholder: '{{task}}', description: 'Task name' },
+ { placeholder: '{{run_name}}', description: 'Run name' },
+ { placeholder: '{{date}}', description: 'Date (YYYY-MM-DD)' },
+ { placeholder: '{{time}}', description: 'Time (HH-MM-SS)' },
+];
+
+interface DynamicArtifactNameInputProps {
+ value: string;
+ onChange: (value: string) => void;
+ disabled?: boolean;
+ placeholder?: string;
+}
+
+export function DynamicArtifactNameInput({
+ value,
+ onChange,
+ disabled = false,
+ placeholder = '{{run_id}}-{{timestamp}}',
+}: DynamicArtifactNameInputProps) {
+ const [isParamsOpen, setIsParamsOpen] = useState(true);
+ const currentValue = value || '';
+
+ const handleInsertPlaceholder = (placeholder: string) => {
+ if (disabled) return;
+ // Insert at the end of current value
+ const newValue = currentValue ? `${currentValue}${placeholder}` : placeholder;
+ onChange(newValue);
+ };
+
+ return (
+
+
onChange(e.target.value)}
+ placeholder={placeholder}
+ className="text-sm font-mono"
+ disabled={disabled}
+ />
+
+ e.g., task123-1617181920
+
+
+ {/* Dynamic Parameters Section */}
+
+
setIsParamsOpen(!isParamsOpen)}
+ className="w-full flex items-center gap-2 px-3 py-2 text-left hover:bg-muted/50 transition-colors"
+ >
+ {isParamsOpen ? (
+
+ ) : (
+
+ )}
+ Dynamic parameters
+
+
+ {isParamsOpen && (
+
+
+ These can be used in the name for the artifact.
+
+
+ {DYNAMIC_PARAMETERS.map((param) => (
+
handleInsertPlaceholder(param.placeholder)}
+ disabled={disabled}
+ className={cn(
+ 'w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left transition-colors',
+ 'hover:bg-primary/10 group',
+ disabled && 'opacity-50 cursor-not-allowed'
+ )}
+ >
+
+
+
+ {param.placeholder}
+
+
+
+ {param.description}
+
+
+ ))}
+
+
+ )}
+
+
+ );
+}
diff --git a/worker/src/components/core/__tests__/artifact-writer.test.ts b/worker/src/components/core/__tests__/artifact-writer.test.ts
index c05f6e25..c8740d58 100644
--- a/worker/src/components/core/__tests__/artifact-writer.test.ts
+++ b/worker/src/components/core/__tests__/artifact-writer.test.ts
@@ -27,7 +27,7 @@ describe('core.artifact.writer component', () => {
const uploadMock = vi.fn().mockResolvedValue({
artifactId: 'artifact-123',
fileId: 'file-123',
- name: 'playground-artifact.txt',
+ name: 'run-log.txt',
destinations: ['run', 'library'],
});
@@ -44,10 +44,11 @@ describe('core.artifact.writer component', () => {
const executePayload = {
inputs: {
+ artifactName: 'run-log',
content: 'Hello artifacts!',
},
params: {
- fileName: 'run-log.txt',
+ fileExtension: '.txt',
mimeType: 'text/plain',
saveToRunArtifacts: true,
publishToArtifactLibrary: true,
@@ -65,16 +66,61 @@ describe('core.artifact.writer component', () => {
expect(result.saved).toBe(true);
expect(result.artifactId).toBe('artifact-123');
+ expect(result.artifactName).toBe('run-log');
+ expect(result.fileName).toBe('run-log.txt');
expect(result.destinations).toEqual(['run', 'library']);
});
+ it('substitutes dynamic placeholders in artifact name', async () => {
+ if (!component) throw new Error('Component not registered');
+
+ const uploadMock = vi.fn().mockResolvedValue({
+ artifactId: 'artifact-456',
+ fileId: 'file-456',
+ name: 'test-artifact.json',
+ destinations: ['run'],
+ });
+
+ const mockArtifacts: IArtifactService = {
+ upload: uploadMock,
+ download: vi.fn(),
+ };
+
+ const context = createExecutionContext({
+ runId: 'test-run-abc123',
+ componentRef: 'artifact-writer-2',
+ artifacts: mockArtifacts,
+ });
+
+ const executePayload = {
+ inputs: {
+ artifactName: '{{run_id}}-{{task}}',
+ content: { data: 'test' },
+ },
+ params: {
+ fileExtension: '.json',
+ mimeType: 'application/json',
+ saveToRunArtifacts: true,
+ publishToArtifactLibrary: false,
+ },
+ };
+
+ const result = await component.execute(executePayload, context);
+
+ expect(uploadMock).toHaveBeenCalledTimes(1);
+ const payload = uploadMock.mock.calls[0][0];
+ expect(payload.name).toBe('test-run-abc123-artifact-writer-2.json');
+ expect(result.artifactName).toBe('test-run-abc123-artifact-writer-2');
+ expect(result.fileName).toBe('test-run-abc123-artifact-writer-2.json');
+ });
+
it('skips upload when no destinations are selected', async () => {
if (!component) throw new Error('Component not registered');
const uploadMock = vi.fn();
const context = createExecutionContext({
runId: 'run-2',
- componentRef: 'artifact-writer-2',
+ componentRef: 'artifact-writer-skip',
artifacts: {
upload: uploadMock,
download: vi.fn(),
@@ -83,10 +129,11 @@ describe('core.artifact.writer component', () => {
const executePayload = {
inputs: {
+ artifactName: 'noop',
content: 'No destinations',
},
params: {
- fileName: 'noop.txt',
+ fileExtension: '.txt',
saveToRunArtifacts: false,
publishToArtifactLibrary: false,
},
@@ -97,6 +144,8 @@ describe('core.artifact.writer component', () => {
expect(uploadMock).not.toHaveBeenCalled();
expect(result.saved).toBe(false);
expect(result.artifactId).toBeUndefined();
+ expect(result.artifactName).toBe('noop');
+ expect(result.fileName).toBe('noop.txt');
expect(result.destinations).toEqual([]);
});
@@ -110,9 +159,11 @@ describe('core.artifact.writer component', () => {
const executePayload = {
inputs: {
+ artifactName: 'test-artifact',
content: 'Need artifacts',
},
params: {
+ fileExtension: '.txt',
saveToRunArtifacts: true,
publishToArtifactLibrary: false,
},
@@ -122,4 +173,47 @@ describe('core.artifact.writer component', () => {
'Artifact service is not available',
);
});
+
+ it('uses default artifact name template when not provided', async () => {
+ if (!component) throw new Error('Component not registered');
+
+ const uploadMock = vi.fn().mockResolvedValue({
+ artifactId: 'artifact-default',
+ fileId: 'file-default',
+ name: 'default.txt',
+ destinations: ['run'],
+ });
+
+ const mockArtifacts: IArtifactService = {
+ upload: uploadMock,
+ download: vi.fn(),
+ };
+
+ const context = createExecutionContext({
+ runId: 'run-default-test',
+ componentRef: 'artifact-writer-default',
+ artifacts: mockArtifacts,
+ });
+
+ const executePayload = {
+ inputs: {
+ // artifactName not provided, should use default template
+ content: 'Default name test',
+ },
+ params: {
+ fileExtension: '.txt',
+ saveToRunArtifacts: true,
+ publishToArtifactLibrary: false,
+ },
+ };
+
+ const result = await component.execute(executePayload, context);
+
+ expect(uploadMock).toHaveBeenCalledTimes(1);
+ const payload = uploadMock.mock.calls[0][0];
+ // Should contain run_id and timestamp pattern
+ expect(payload.name).toMatch(/^run-default-test-\d+\.txt$/);
+ expect(result.artifactName).toMatch(/^run-default-test-\d+$/);
+ expect(result.saved).toBe(true);
+ });
});
diff --git a/worker/src/components/core/artifact-writer.ts b/worker/src/components/core/artifact-writer.ts
index 292741e2..698ea9a0 100644
--- a/worker/src/components/core/artifact-writer.ts
+++ b/worker/src/components/core/artifact-writer.ts
@@ -11,6 +11,20 @@ import {
} from '@shipsec/component-sdk';
const inputSchema = inputs({
+ artifactName: port(
+ z
+ .string()
+ .optional()
+ .describe(
+ 'Name for the artifact. Supports dynamic placeholders: {{run_id}}, {{timestamp}}, {{dataset}}, {{task}}, {{run_name}}. Defaults to {{run_id}}-{{timestamp}}.',
+ ),
+ {
+ label: 'Artifact Name',
+ description:
+ 'Name for the artifact file. Use dynamic placeholders like {{run_id}}, {{timestamp}} for unique names.',
+ editor: 'text',
+ },
+ ),
content: port(
z
.any()
@@ -29,17 +43,50 @@ const inputSchema = inputs({
),
});
+/**
+ * Substitutes dynamic placeholders in artifact name template.
+ * Supported placeholders:
+ * - {{run_id}} - Current run ID
+ * - {{timestamp}} - Unix timestamp in milliseconds
+ * - {{dataset}} - Dataset name (if available)
+ * - {{task}} - Task name (if available)
+ * - {{run_name}} - Human-readable run name
+ * - {{date}} - ISO date (YYYY-MM-DD)
+ * - {{time}} - ISO time (HH-MM-SS)
+ */
+function substituteArtifactName(
+ template: string,
+ context: { runId: string; componentRef: string },
+): string {
+ const now = new Date();
+ const timestamp = now.getTime().toString();
+ const isoDate = now.toISOString().split('T')[0]; // YYYY-MM-DD
+ const isoTime = now.toISOString().split('T')[1].split('.')[0].replace(/:/g, '-'); // HH-MM-SS
+
+ // Extract a shorter run name from runId (last segment after last hyphen, or first 8 chars)
+ const runIdParts = context.runId.split('-');
+ const runName = runIdParts.length > 1 ? runIdParts.slice(-1)[0] : context.runId.slice(0, 8);
+
+ return template
+ .replace(/\{\{run_id\}\}/gi, context.runId)
+ .replace(/\{\{timestamp\}\}/gi, timestamp)
+ .replace(/\{\{dataset\}\}/gi, 'default')
+ .replace(/\{\{task\}\}/gi, context.componentRef)
+ .replace(/\{\{run_name\}\}/gi, runName)
+ .replace(/\{\{date\}\}/gi, isoDate)
+ .replace(/\{\{time\}\}/gi, isoTime);
+}
+
const parameterSchema = parameters({
- fileName: param(
+ fileExtension: param(
z
.string()
- .min(1, 'File name is required')
- .default('artifact.txt')
- .describe('File name to assign to the saved artifact.'),
+ .default('.txt')
+ .describe('File extension to append to the artifact name.'),
{
- label: 'File Name',
+ label: 'File Extension',
editor: 'text',
- description: 'File name to use when saving the artifact.',
+ description: 'File extension (e.g., .txt, .json, .csv). Will be appended to the artifact name.',
},
),
mimeType: param(
@@ -76,9 +123,13 @@ const outputSchema = outputs({
label: 'Artifact ID',
description: 'Identifier returned by the artifact service.',
}),
+ artifactName: port(z.string(), {
+ label: 'Artifact Name',
+ description: 'Resolved name of the artifact (with placeholders substituted).',
+ }),
fileName: port(z.string(), {
label: 'File Name',
- description: 'Name of the artifact file that was written.',
+ description: 'Full file name including extension.',
}),
size: port(z.number(), {
label: 'Size',
@@ -125,6 +176,19 @@ const definition = defineComponent({
destinations.push('library');
}
+ // Resolve artifact name with dynamic placeholders
+ const artifactNameTemplate = inputs.artifactName || '{{run_id}}-{{timestamp}}';
+ const resolvedArtifactName = substituteArtifactName(artifactNameTemplate, {
+ runId: context.runId,
+ componentRef: context.componentRef,
+ });
+
+ // Build full filename with extension
+ const extension = params.fileExtension || '.txt';
+ const fileName = resolvedArtifactName.endsWith(extension)
+ ? resolvedArtifactName
+ : `${resolvedArtifactName}${extension}`;
+
// Serialize content to string - if already a string, use as-is; otherwise JSON stringify
const rawContent = inputs.content;
let serializedContent: string;
@@ -141,7 +205,8 @@ const definition = defineComponent({
context.logger.info('[ArtifactWriter] No destinations selected; skipping upload.');
return {
artifactId: undefined,
- fileName: params.fileName,
+ artifactName: resolvedArtifactName,
+ fileName,
size: Buffer.byteLength(serializedContent),
destinations: [],
saved: false,
@@ -157,13 +222,13 @@ const definition = defineComponent({
const buffer = Buffer.from(serializedContent, 'utf-8');
context.logger.info(
- `[ArtifactWriter] Uploading '${params.fileName}' (${buffer.byteLength} bytes) to ${destinations.join(
+ `[ArtifactWriter] Uploading '${fileName}' (${buffer.byteLength} bytes) to ${destinations.join(
', ',
)}`,
);
const upload = await context.artifacts.upload({
- name: params.fileName,
+ name: fileName,
mimeType: params.mimeType ?? 'text/plain',
content: buffer,
destinations,
@@ -171,7 +236,8 @@ const definition = defineComponent({
return {
artifactId: upload.artifactId,
- fileName: params.fileName,
+ artifactName: resolvedArtifactName,
+ fileName,
size: buffer.byteLength,
destinations,
saved: true,
From cf75d6c254483bb8d6537203c9ea512cb0372b2e Mon Sep 17 00:00:00 2001
From: Krishna Mohan
Date: Tue, 27 Jan 2026 17:09:33 +0530
Subject: [PATCH 2/5] fix: linting issue fixed
Signed-off-by: Krishna Mohan
---
frontend/src/components/workflow/ConfigPanel.tsx | 9 +++++----
.../workflow/DynamicArtifactNameInput.tsx | 16 +++++-----------
worker/src/components/core/artifact-writer.ts | 8 +++-----
3 files changed, 13 insertions(+), 20 deletions(-)
diff --git a/frontend/src/components/workflow/ConfigPanel.tsx b/frontend/src/components/workflow/ConfigPanel.tsx
index ca6fd0f8..ae469aa3 100644
--- a/frontend/src/components/workflow/ConfigPanel.tsx
+++ b/frontend/src/components/workflow/ConfigPanel.tsx
@@ -955,8 +955,10 @@ export function ConfigPanel({
/>
)}
{/* Skip helper text for DynamicArtifactNameInput as it has its own */}
- {!(component?.id === 'core.artifact.writer' && input.id === 'artifactName') && (
- manualLocked ? (
+ {!(
+ component?.id === 'core.artifact.writer' && input.id === 'artifactName'
+ ) &&
+ (manualLocked ? (
Disconnect the port to edit manual input.
@@ -968,8 +970,7 @@ export function ConfigPanel({
? 'Add entries or clear manual input to require a port connection.'
: 'Leave blank to require a port connection.'}
- )
- )}
+ ))}
)}
diff --git a/frontend/src/components/workflow/DynamicArtifactNameInput.tsx b/frontend/src/components/workflow/DynamicArtifactNameInput.tsx
index d179fc64..81068740 100644
--- a/frontend/src/components/workflow/DynamicArtifactNameInput.tsx
+++ b/frontend/src/components/workflow/DynamicArtifactNameInput.tsx
@@ -51,9 +51,7 @@ export function DynamicArtifactNameInput({
className="text-sm font-mono"
disabled={disabled}
/>
-
- e.g., task123-1617181920
-
+ e.g., task123-1617181920
{/* Dynamic Parameters Section */}
@@ -85,7 +83,7 @@ export function DynamicArtifactNameInput({
className={cn(
'w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left transition-colors',
'hover:bg-primary/10 group',
- disabled && 'opacity-50 cursor-not-allowed'
+ disabled && 'opacity-50 cursor-not-allowed',
)}
>
-
- {param.placeholder}
-
+ {param.placeholder}
-
- {param.description}
-
+
{param.description}
))}
diff --git a/worker/src/components/core/artifact-writer.ts b/worker/src/components/core/artifact-writer.ts
index 698ea9a0..9f2db346 100644
--- a/worker/src/components/core/artifact-writer.ts
+++ b/worker/src/components/core/artifact-writer.ts
@@ -79,14 +79,12 @@ function substituteArtifactName(
const parameterSchema = parameters({
fileExtension: param(
- z
- .string()
- .default('.txt')
- .describe('File extension to append to the artifact name.'),
+ z.string().default('.txt').describe('File extension to append to the artifact name.'),
{
label: 'File Extension',
editor: 'text',
- description: 'File extension (e.g., .txt, .json, .csv). Will be appended to the artifact name.',
+ description:
+ 'File extension (e.g., .txt, .json, .csv). Will be appended to the artifact name.',
},
),
mimeType: param(
From 1e1455231c1a0d3eeb44e06544cf42cd5fd21e2f Mon Sep 17 00:00:00 2001
From: Krishna Mohan
Date: Fri, 30 Jan 2026 14:16:20 +0530
Subject: [PATCH 3/5] fix: fixed the placeholder naming option match
Signed-off-by: Krishna Mohan
---
.../workflow/DynamicArtifactNameInput.tsx | 100 +++++++-----------
worker/src/components/core/artifact-writer.ts | 16 +--
2 files changed, 41 insertions(+), 75 deletions(-)
diff --git a/frontend/src/components/workflow/DynamicArtifactNameInput.tsx b/frontend/src/components/workflow/DynamicArtifactNameInput.tsx
index 81068740..14193a65 100644
--- a/frontend/src/components/workflow/DynamicArtifactNameInput.tsx
+++ b/frontend/src/components/workflow/DynamicArtifactNameInput.tsx
@@ -1,7 +1,11 @@
-import { useState } from 'react';
-import { ChevronDown, ChevronRight, Plus } from 'lucide-react';
import { Input } from '@/components/ui/input';
-import { cn } from '@/lib/utils';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
interface DynamicParameter {
placeholder: string;
@@ -9,11 +13,9 @@ interface DynamicParameter {
}
const DYNAMIC_PARAMETERS: DynamicParameter[] = [
- { placeholder: '{{timestamp}}', description: 'Current timestamp' },
- { placeholder: '{{run_id}}', description: 'Run ID' },
- { placeholder: '{{dataset}}', description: 'Dataset name' },
- { placeholder: '{{task}}', description: 'Task name' },
- { placeholder: '{{run_name}}', description: 'Run name' },
+ { placeholder: '{{run_id}}', description: 'Full workflow run ID' },
+ { placeholder: '{{node_id}}', description: 'Component node ID in workflow' },
+ { placeholder: '{{timestamp}}', description: 'Unix timestamp (ms)' },
{ placeholder: '{{date}}', description: 'Date (YYYY-MM-DD)' },
{ placeholder: '{{time}}', description: 'Time (HH-MM-SS)' },
];
@@ -31,13 +33,12 @@ export function DynamicArtifactNameInput({
disabled = false,
placeholder = '{{run_id}}-{{timestamp}}',
}: DynamicArtifactNameInputProps) {
- const [isParamsOpen, setIsParamsOpen] = useState(true);
const currentValue = value || '';
- const handleInsertPlaceholder = (placeholder: string) => {
+ const handleInsertPlaceholder = (selectedPlaceholder: string) => {
if (disabled) return;
// Insert at the end of current value
- const newValue = currentValue ? `${currentValue}${placeholder}` : placeholder;
+ const newValue = currentValue ? `${currentValue}${selectedPlaceholder}` : selectedPlaceholder;
onChange(newValue);
};
@@ -51,61 +52,34 @@ export function DynamicArtifactNameInput({
className="text-sm font-mono"
disabled={disabled}
/>
- e.g., task123-1617181920
- {/* Dynamic Parameters Section */}
-
-
setIsParamsOpen(!isParamsOpen)}
- className="w-full flex items-center gap-2 px-3 py-2 text-left hover:bg-muted/50 transition-colors"
+
+
- {isParamsOpen ? (
-
- ) : (
-
- )}
- Dynamic parameters
-
-
- {isParamsOpen && (
-
-
- These can be used in the name for the artifact.
-
-
- {DYNAMIC_PARAMETERS.map((param) => (
-
handleInsertPlaceholder(param.placeholder)}
- disabled={disabled}
- className={cn(
- 'w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left transition-colors',
- 'hover:bg-primary/10 group',
- disabled && 'opacity-50 cursor-not-allowed',
- )}
- >
-
-
- {param.placeholder}
-
- {param.description}
-
- ))}
-
-
- )}
+
+
+
+
+ {DYNAMIC_PARAMETERS.map((param) => (
+
+ {param.placeholder}
+ — {param.description}
+
+ ))}
+
+
+
+
+ Type directly or select a placeholder to insert. Example: scan-{'{{date}}'}-{'{{time}}'}
+
);
}
diff --git a/worker/src/components/core/artifact-writer.ts b/worker/src/components/core/artifact-writer.ts
index 9f2db346..a31d51fe 100644
--- a/worker/src/components/core/artifact-writer.ts
+++ b/worker/src/components/core/artifact-writer.ts
@@ -16,7 +16,7 @@ const inputSchema = inputs({
.string()
.optional()
.describe(
- 'Name for the artifact. Supports dynamic placeholders: {{run_id}}, {{timestamp}}, {{dataset}}, {{task}}, {{run_name}}. Defaults to {{run_id}}-{{timestamp}}.',
+ 'Name for the artifact. Supports dynamic placeholders: {{run_id}}, {{node_id}}, {{timestamp}}, {{date}}, {{time}}. Defaults to {{run_id}}-{{timestamp}}.',
),
{
label: 'Artifact Name',
@@ -46,11 +46,9 @@ const inputSchema = inputs({
/**
* Substitutes dynamic placeholders in artifact name template.
* Supported placeholders:
- * - {{run_id}} - Current run ID
+ * - {{run_id}} - Full workflow run ID
+ * - {{node_id}} - Component's node ID in the workflow (e.g., "artifact-writer-1")
* - {{timestamp}} - Unix timestamp in milliseconds
- * - {{dataset}} - Dataset name (if available)
- * - {{task}} - Task name (if available)
- * - {{run_name}} - Human-readable run name
* - {{date}} - ISO date (YYYY-MM-DD)
* - {{time}} - ISO time (HH-MM-SS)
*/
@@ -63,16 +61,10 @@ function substituteArtifactName(
const isoDate = now.toISOString().split('T')[0]; // YYYY-MM-DD
const isoTime = now.toISOString().split('T')[1].split('.')[0].replace(/:/g, '-'); // HH-MM-SS
- // Extract a shorter run name from runId (last segment after last hyphen, or first 8 chars)
- const runIdParts = context.runId.split('-');
- const runName = runIdParts.length > 1 ? runIdParts.slice(-1)[0] : context.runId.slice(0, 8);
-
return template
.replace(/\{\{run_id\}\}/gi, context.runId)
+ .replace(/\{\{node_id\}\}/gi, context.componentRef)
.replace(/\{\{timestamp\}\}/gi, timestamp)
- .replace(/\{\{dataset\}\}/gi, 'default')
- .replace(/\{\{task\}\}/gi, context.componentRef)
- .replace(/\{\{run_name\}\}/gi, runName)
.replace(/\{\{date\}\}/gi, isoDate)
.replace(/\{\{time\}\}/gi, isoTime);
}
From df3e16c9f8751c230dc09c2ca536272f1d224373 Mon Sep 17 00:00:00 2001
From: Krishna Mohan
Date: Fri, 30 Jan 2026 14:18:33 +0530
Subject: [PATCH 4/5] fix: fixed linting issue
Signed-off-by: Krishna Mohan
---
.../components/workflow/DynamicArtifactNameInput.tsx | 12 ++----------
1 file changed, 2 insertions(+), 10 deletions(-)
diff --git a/frontend/src/components/workflow/DynamicArtifactNameInput.tsx b/frontend/src/components/workflow/DynamicArtifactNameInput.tsx
index 14193a65..0f838ba0 100644
--- a/frontend/src/components/workflow/DynamicArtifactNameInput.tsx
+++ b/frontend/src/components/workflow/DynamicArtifactNameInput.tsx
@@ -54,21 +54,13 @@ export function DynamicArtifactNameInput({
/>
-
+
{DYNAMIC_PARAMETERS.map((param) => (
-
+
{param.placeholder}
— {param.description}
From 867151c6deb2976ed8365beda0f0a1695016e491 Mon Sep 17 00:00:00 2001
From: Krishna Mohan
Date: Fri, 30 Jan 2026 14:23:18 +0530
Subject: [PATCH 5/5] fix: changed the test case option for artifact naming
Signed-off-by: Krishna Mohan
---
worker/src/components/core/__tests__/artifact-writer.test.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/worker/src/components/core/__tests__/artifact-writer.test.ts b/worker/src/components/core/__tests__/artifact-writer.test.ts
index c8740d58..d70e39b8 100644
--- a/worker/src/components/core/__tests__/artifact-writer.test.ts
+++ b/worker/src/components/core/__tests__/artifact-writer.test.ts
@@ -94,7 +94,7 @@ describe('core.artifact.writer component', () => {
const executePayload = {
inputs: {
- artifactName: '{{run_id}}-{{task}}',
+ artifactName: '{{run_id}}-{{node_id}}',
content: { data: 'test' },
},
params: {