diff --git a/internal/admin/dashboard/static/css/dashboard.css b/internal/admin/dashboard/static/css/dashboard.css
index dc77fcbe..8129a121 100644
--- a/internal/admin/dashboard/static/css/dashboard.css
+++ b/internal/admin/dashboard/static/css/dashboard.css
@@ -4202,6 +4202,34 @@ body.conversation-drawer-open {
color: var(--success);
}
+.workflow-node-warning {
+ border-color: color-mix(in srgb, var(--warning) 52%, var(--border));
+ background: color-mix(in srgb, var(--warning) 9%, var(--bg-surface));
+}
+
+.workflow-node-warning .workflow-node-icon {
+ background: color-mix(in srgb, var(--warning) 14%, var(--bg));
+ color: var(--warning);
+}
+
+.workflow-node-warning .workflow-node-icon-endpoint {
+ color: var(--warning);
+}
+
+.workflow-node-warning .workflow-node-label {
+ color: color-mix(in srgb, var(--warning) 85%, var(--text));
+}
+
+.workflow-node-warning .workflow-node-sub {
+ color: color-mix(in srgb, var(--warning) 72%, var(--text-muted));
+}
+
+.workflow-node-warning .workflow-node-badge {
+ background: color-mix(in srgb, var(--warning) 14%, var(--bg));
+ border-color: color-mix(in srgb, var(--warning) 38%, var(--border));
+ color: var(--warning);
+}
+
.workflow-node-error {
border-color: color-mix(in srgb, var(--danger) 52%, var(--border));
background: color-mix(in srgb, var(--danger) 9%, var(--bg-surface));
@@ -4224,6 +4252,34 @@ body.conversation-drawer-open {
color: color-mix(in srgb, var(--danger) 72%, var(--text-muted));
}
+.workflow-node-neutral {
+ border-color: color-mix(in srgb, var(--text-muted) 40%, var(--border));
+ background: color-mix(in srgb, var(--text-muted) 8%, var(--bg-surface));
+}
+
+.workflow-node-neutral .workflow-node-icon {
+ background: color-mix(in srgb, var(--text-muted) 12%, var(--bg));
+ color: var(--text-muted);
+}
+
+.workflow-node-neutral .workflow-node-icon-endpoint {
+ color: var(--text-muted);
+}
+
+.workflow-node-neutral .workflow-node-label {
+ color: var(--text-muted);
+}
+
+.workflow-node-neutral .workflow-node-sub {
+ color: color-mix(in srgb, var(--text-muted) 84%, var(--border));
+}
+
+.workflow-node-neutral .workflow-node-badge {
+ background: color-mix(in srgb, var(--text-muted) 10%, var(--bg));
+ border-color: color-mix(in srgb, var(--text-muted) 28%, var(--border));
+ color: var(--text-muted);
+}
+
.workflow-node-skipped {
position: relative;
opacity: 0.28;
diff --git a/internal/admin/dashboard/static/js/modules/workflows.js b/internal/admin/dashboard/static/js/modules/workflows.js
index aa62bfcb..9131ab68 100644
--- a/internal/admin/dashboard/static/js/modules/workflows.js
+++ b/internal/admin/dashboard/static/js/modules/workflows.js
@@ -1234,6 +1234,7 @@
aiNodeClass: this.workflowAiNodeClass(runtime),
responseConnClass: this.workflowResponseConnClass(runtime),
responseNodeClass: this.workflowResponseNodeClass(runtime),
+ responseNodeSublabel: this.workflowResponseNodeSublabel(runtime),
authNodeClass: this.workflowAuthNodeClass(runtime),
authNodeSublabel: this.workflowAuthNodeSublabel(runtime),
usageNodeClass: this.workflowAsyncNodeClass(showUsage, highlightAsyncPresent),
@@ -1345,7 +1346,18 @@
workflowResponseNodeClass(runtime) {
if (!runtime) return '';
- return runtime.responseSuccess ? 'workflow-node-success' : '';
+ const statusCode = runtime.statusCode;
+ if (!Number.isFinite(statusCode)) return '';
+ if (statusCode >= 500) return 'workflow-node-error';
+ if (statusCode >= 400) return 'workflow-node-warning';
+ if (statusCode >= 300) return 'workflow-node-neutral';
+ if (statusCode >= 200) return 'workflow-node-success';
+ return '';
+ },
+
+ workflowResponseNodeSublabel(runtime) {
+ if (!runtime || !Number.isFinite(runtime.statusCode)) return null;
+ return String(runtime.statusCode);
},
workflowAuthNodeClass(runtime) {
diff --git a/internal/admin/dashboard/static/js/modules/workflows.test.js b/internal/admin/dashboard/static/js/modules/workflows.test.js
index dd012e51..19240ca6 100644
--- a/internal/admin/dashboard/static/js/modules/workflows.test.js
+++ b/internal/admin/dashboard/static/js/modules/workflows.test.js
@@ -230,6 +230,7 @@ test('workflowChart returns the shared chart contract for workflow sources', ()
aiNodeClass: '',
responseConnClass: '',
responseNodeClass: '',
+ responseNodeSublabel: null,
authNodeClass: '',
authNodeSublabel: null,
usageNodeClass: '',
@@ -290,6 +291,7 @@ test('workflowChart masks globally disabled workflow features from persisted wor
aiNodeClass: '',
responseConnClass: '',
responseNodeClass: '',
+ responseNodeSublabel: null,
authNodeClass: '',
authNodeSublabel: null,
usageNodeClass: '',
@@ -439,6 +441,7 @@ test('workflowAuditChart returns the shared chart contract for audit runtime ent
aiNodeClass: 'workflow-node-skipped',
responseConnClass: 'workflow-conn-dim',
responseNodeClass: 'workflow-node-success',
+ responseNodeSublabel: '200',
authNodeClass: '',
authNodeSublabel: null,
usageNodeClass: 'workflow-node-success',
@@ -480,6 +483,7 @@ test('workflowAuditChart forces audit nodes even when the workflow version canno
aiNodeClass: 'workflow-node-skipped',
responseConnClass: 'workflow-conn-dim',
responseNodeClass: 'workflow-node-success',
+ responseNodeSublabel: '200',
authNodeClass: '',
authNodeSublabel: null,
usageNodeClass: '',
@@ -550,6 +554,7 @@ test('workflowAuditChart prefers request-time workflow features over current wor
aiNodeClass: 'workflow-node-success',
responseConnClass: '',
responseNodeClass: 'workflow-node-success',
+ responseNodeSublabel: '200',
authNodeClass: '',
authNodeSublabel: null,
usageNodeClass: '',
@@ -621,6 +626,7 @@ test('workflowAuditChart highlights configured failover redirects and exposes th
aiNodeClass: 'workflow-node-success',
responseConnClass: '',
responseNodeClass: 'workflow-node-success',
+ responseNodeSublabel: '200',
authNodeClass: '',
authNodeSublabel: null,
usageNodeClass: 'workflow-node-success',
@@ -1900,6 +1906,7 @@ test('audit runtime uses explicit cache-hit labels and highlights the uncached 2
assert.equal(module.workflowAiNodeClass(semanticHit), 'workflow-node-skipped');
assert.equal(module.workflowResponseConnClass(semanticHit), 'workflow-conn-dim');
assert.equal(module.workflowResponseNodeClass(semanticHit), 'workflow-node-success');
+ assert.equal(module.workflowResponseNodeSublabel(semanticHit), '200');
const uncachedSuccess = module.workflowRuntimeFromEntry({
provider: 'openai',
@@ -1913,6 +1920,39 @@ test('audit runtime uses explicit cache-hit labels and highlights the uncached 2
assert.equal(module.workflowAiNodeClass(uncachedSuccess), 'workflow-node-success');
assert.equal(module.workflowResponseConnClass(uncachedSuccess), '');
assert.equal(module.workflowResponseNodeClass(uncachedSuccess), 'workflow-node-success');
+ assert.equal(module.workflowResponseNodeSublabel(uncachedSuccess), '200');
+});
+
+test('response runtime maps 3xx and 4xx statuses to neutral and warning chart colors', () => {
+ const module = createWorkflowsModule();
+
+ const redirect = module.workflowRuntimeFromEntry({
+ provider: 'openai',
+ model: 'gpt-5',
+ status_code: 304
+ });
+ assert.equal(module.workflowResponseNodeClass(redirect), 'workflow-node-neutral');
+ assert.equal(module.workflowResponseNodeSublabel(redirect), '304');
+
+ const clientError = module.workflowRuntimeFromEntry({
+ provider: 'openai',
+ model: 'gpt-5',
+ status_code: 429
+ });
+ assert.equal(module.workflowResponseNodeClass(clientError), 'workflow-node-warning');
+ assert.equal(module.workflowResponseNodeSublabel(clientError), '429');
+});
+
+test('response runtime maps 5xx statuses to the error chart color', () => {
+ const module = createWorkflowsModule();
+
+ const serverError = module.workflowRuntimeFromEntry({
+ provider: 'openai',
+ model: 'gpt-5',
+ status_code: 503
+ });
+ assert.equal(module.workflowResponseNodeClass(serverError), 'workflow-node-error');
+ assert.equal(module.workflowResponseNodeSublabel(serverError), '503');
});
test('workflowRuntimeFromEntry treats any uncached 2xx status as a successful AI and response path', () => {
diff --git a/internal/admin/dashboard/templates/workflow-chart.html b/internal/admin/dashboard/templates/workflow-chart.html
index 4f29a844..b27ce992 100644
--- a/internal/admin/dashboard/templates/workflow-chart.html
+++ b/internal/admin/dashboard/templates/workflow-chart.html
@@ -74,6 +74,7 @@
Response
+