From 2a13ff93946919d389a94ff4d2f011ff8838a954 Mon Sep 17 00:00:00 2001 From: Raphael Lechner Date: Mon, 13 Apr 2026 15:17:52 +0200 Subject: [PATCH 1/3] feat(workflows): support explicit tags and category in workflow YAML Allow workflow authors to define custom tags and category directly in their YAML files. This enables community adapters (e.g. GitLab, Gitea) and custom workflows to control their own metadata without requiring changes to the core keyword-detection logic. Explicit values take precedence; keyword-based inference remains as the fallback for workflows without these fields. Example usage: tags: [GitLab, Review] category: Code Review --- .../src/components/workflows/WorkflowCard.tsx | 9 ++++-- .../src/components/workflows/WorkflowList.tsx | 6 +++- .../web/src/lib/workflow-metadata.test.ts | 31 +++++++++++++++++++ packages/web/src/lib/workflow-metadata.ts | 25 +++++++++++++-- packages/workflows/src/schemas/workflow.ts | 2 ++ 5 files changed, 67 insertions(+), 6 deletions(-) diff --git a/packages/web/src/components/workflows/WorkflowCard.tsx b/packages/web/src/components/workflows/WorkflowCard.tsx index 10ed0cd23e..5bc805965e 100644 --- a/packages/web/src/components/workflows/WorkflowCard.tsx +++ b/packages/web/src/components/workflows/WorkflowCard.tsx @@ -54,8 +54,13 @@ export function WorkflowCard({ }: WorkflowCardProps): React.ReactElement { const parsed = parseWorkflowDescription(workflow.description ?? ''); const displayName = getWorkflowDisplayName(workflow.name); - const category = getWorkflowCategory(workflow.name, workflow.description ?? ''); - const tags = getWorkflowTags(workflow.name, parsed); + const wf = workflow as Record; + const category = getWorkflowCategory( + workflow.name, + workflow.description ?? '', + wf.category as string | undefined + ); + const tags = getWorkflowTags(workflow.name, parsed, wf.tags as string[] | undefined); const iconName = getWorkflowIconName(workflow.name, category); const CARD_ICON = ICON_MAP[iconName]; diff --git a/packages/web/src/components/workflows/WorkflowList.tsx b/packages/web/src/components/workflows/WorkflowList.tsx index ed837f7929..91b1bde169 100644 --- a/packages/web/src/components/workflows/WorkflowList.tsx +++ b/packages/web/src/components/workflows/WorkflowList.tsx @@ -106,7 +106,11 @@ export function WorkflowList(): React.ReactElement { } // Category filter if (activeCategory !== 'All') { - const cat = getWorkflowCategory(wf.name, wf.description ?? ''); + const cat = getWorkflowCategory( + wf.name, + wf.description ?? '', + (wf as Record).category as string | undefined + ); if (cat !== activeCategory) return false; } return true; diff --git a/packages/web/src/lib/workflow-metadata.test.ts b/packages/web/src/lib/workflow-metadata.test.ts index 18af743267..3ab50efbff 100644 --- a/packages/web/src/lib/workflow-metadata.test.ts +++ b/packages/web/src/lib/workflow-metadata.test.ts @@ -174,6 +174,18 @@ describe('getWorkflowCategory', () => { expect(getWorkflowCategory('archon-assist', 'General help')).toBe('Development'); expect(getWorkflowCategory('archon-idea-to-pr', 'From idea to PR')).toBe('Development'); }); + + test('uses explicit category when provided', () => { + expect(getWorkflowCategory('my-gitlab-workflow', 'Does GitLab things', 'Automation')).toBe( + 'Automation' + ); + }); + + test('falls back to inference when explicit category is invalid', () => { + expect(getWorkflowCategory('archon-smart-pr-review', 'Review PR', 'InvalidCategory')).toBe( + 'Code Review' + ); + }); }); describe('getWorkflowTags', () => { @@ -200,6 +212,25 @@ describe('getWorkflowTags', () => { const githubCount = tags.filter(t => t === 'GitHub').length; expect(githubCount).toBeLessThanOrEqual(1); }); + + test('uses explicit tags when provided', () => { + const parsed = parseWorkflowDescription('A GitLab workflow'); + const tags = getWorkflowTags('review-gitlab-mr', parsed, ['GitLab', 'Review']); + expect(tags).toEqual(['GitLab', 'Review']); + }); + + test('falls back to inference when no explicit tags', () => { + const parsed = parseWorkflowDescription('Does: review PR on GitHub'); + const tags = getWorkflowTags('archon-pr-review', parsed, undefined); + expect(tags).toContain('GitHub'); + expect(tags).toContain('Review'); + }); + + test('deduplicates explicit tags', () => { + const parsed = parseWorkflowDescription('anything'); + const tags = getWorkflowTags('test', parsed, ['GitLab', 'GitLab', 'Review']); + expect(tags).toEqual(['GitLab', 'Review']); + }); }); describe('getWorkflowIconName', () => { diff --git a/packages/web/src/lib/workflow-metadata.ts b/packages/web/src/lib/workflow-metadata.ts index e3ab01191d..7b103e6915 100644 --- a/packages/web/src/lib/workflow-metadata.ts +++ b/packages/web/src/lib/workflow-metadata.ts @@ -119,9 +119,18 @@ export const CATEGORIES: WorkflowCategory[] = [ /** * Derive a category from the workflow name and description. - * Uses word-boundary checks for short tokens to avoid false positives. + * If the workflow defines an explicit `category` in YAML that matches a known + * category, it is used directly. Otherwise, the category is inferred from keywords. */ -export function getWorkflowCategory(name: string, description: string): WorkflowCategory { +export function getWorkflowCategory( + name: string, + description: string, + explicitCategory?: string +): WorkflowCategory { + if (explicitCategory && CATEGORIES.includes(explicitCategory as WorkflowCategory)) { + return explicitCategory as WorkflowCategory; + } + const lower = `${name} ${description}`.toLowerCase(); // Code Review @@ -163,8 +172,18 @@ export function getWorkflowCategory(name: string, description: string): Workflow /** * Derive tags from the workflow name and parsed description. + * If the workflow defines explicit `tags` in YAML, those are used directly. + * Otherwise, tags are inferred from keywords in the name and description. */ -export function getWorkflowTags(name: string, parsed: ParsedDescription): string[] { +export function getWorkflowTags( + name: string, + parsed: ParsedDescription, + explicitTags?: string[] +): string[] { + if (explicitTags && explicitTags.length > 0) { + return [...new Set(explicitTags)]; + } + const tags: string[] = []; const text = `${name} ${parsed.raw}`.toLowerCase(); diff --git a/packages/workflows/src/schemas/workflow.ts b/packages/workflows/src/schemas/workflow.ts index 008ef19a8f..b42b086afa 100644 --- a/packages/workflows/src/schemas/workflow.ts +++ b/packages/workflows/src/schemas/workflow.ts @@ -40,6 +40,8 @@ export const workflowBaseSchema = z.object({ fallbackModel: z.string().min(1).optional(), betas: z.array(z.string().min(1)).nonempty("'betas' must be a non-empty array").optional(), sandbox: sandboxSettingsSchema.optional(), + tags: z.array(z.string().min(1)).optional(), + category: z.string().min(1).optional(), }); export type WorkflowBase = z.infer; From c6aa9e4dbdc07edab4a09fe9caf497ff6c41d23d Mon Sep 17 00:00:00 2001 From: Raphael Lechner Date: Mon, 13 Apr 2026 15:24:41 +0200 Subject: [PATCH 2/3] fix(workflows): pass tags and category through loader to API The workflow loader manually constructs the result object and was not including the new tags and category fields from parsed YAML. Add them to the loader output so they flow through to the API response. --- packages/workflows/src/loader.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/workflows/src/loader.ts b/packages/workflows/src/loader.ts index 0fd93cce1f..622a023760 100644 --- a/packages/workflows/src/loader.ts +++ b/packages/workflows/src/loader.ts @@ -335,6 +335,19 @@ export function parseWorkflow(content: string, filename: string): ParseResult { } } + // Parse optional tags — warn on non-strings + const tags = Array.isArray(raw.tags) + ? raw.tags.filter((t: unknown) => { + if (typeof t !== 'string') { + getLog().warn({ filename, value: t }, 'non_string_tag_filtered'); + return false; + } + return true; + }) + : undefined; + + const category = typeof raw.category === 'string' ? raw.category : undefined; + return { workflow: { name: raw.name, @@ -345,6 +358,8 @@ export function parseWorkflow(content: string, filename: string): ParseResult { webSearchMode, additionalDirectories, interactive, + tags, + category, nodes: dagNodes, }, error: null, From 85b7507e4eb5786e853f8924ea621446a969e0f2 Mon Sep 17 00:00:00 2001 From: Raphael Lechner Date: Mon, 13 Apr 2026 15:26:45 +0200 Subject: [PATCH 3/3] fix(web): treat explicit empty tags array as override An explicit `tags: []` in YAML should suppress keyword inference, allowing authors to clear mis-inferred tags. Previously the empty array fell through to the fallback logic. --- packages/web/src/lib/workflow-metadata.test.ts | 6 ++++++ packages/web/src/lib/workflow-metadata.ts | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/web/src/lib/workflow-metadata.test.ts b/packages/web/src/lib/workflow-metadata.test.ts index 3ab50efbff..656bbc390d 100644 --- a/packages/web/src/lib/workflow-metadata.test.ts +++ b/packages/web/src/lib/workflow-metadata.test.ts @@ -231,6 +231,12 @@ describe('getWorkflowTags', () => { const tags = getWorkflowTags('test', parsed, ['GitLab', 'GitLab', 'Review']); expect(tags).toEqual(['GitLab', 'Review']); }); + + test('explicit empty array suppresses inference', () => { + const parsed = parseWorkflowDescription('Does: review PR on GitHub'); + const tags = getWorkflowTags('archon-pr-review', parsed, []); + expect(tags).toEqual([]); + }); }); describe('getWorkflowIconName', () => { diff --git a/packages/web/src/lib/workflow-metadata.ts b/packages/web/src/lib/workflow-metadata.ts index 7b103e6915..9049613ca1 100644 --- a/packages/web/src/lib/workflow-metadata.ts +++ b/packages/web/src/lib/workflow-metadata.ts @@ -180,7 +180,7 @@ export function getWorkflowTags( parsed: ParsedDescription, explicitTags?: string[] ): string[] { - if (explicitTags && explicitTags.length > 0) { + if (explicitTags !== undefined) { return [...new Set(explicitTags)]; }