= ({ workflow, trigger, onMutate, onRe
)}
+ {src.kind === 'stream' && (
+
+ )}
+
{src.kind === 'custom' && (
Anything can push events here (scripts, webhooks, Shortcuts):
diff --git a/frontend/src/app/pages/Workflows/app/EventTriggersCard.tsx b/frontend/src/app/pages/Workflows/app/EventTriggersCard.tsx
index 7249c456..e4093a76 100644
--- a/frontend/src/app/pages/Workflows/app/EventTriggersCard.tsx
+++ b/frontend/src/app/pages/Workflows/app/EventTriggersCard.tsx
@@ -12,19 +12,21 @@ import EventTriggerRow from './EventTriggerRow';
import { useWC, FONT_SERIF } from './uiKit';
import { useWorkflowPatch } from './useWorkflowPatch';
-type TriggerKind = 'file' | 'web' | 'agent' | 'custom';
+type TriggerKind = 'file' | 'web' | 'agent' | 'custom' | 'stream';
const ADD_CHOICES: Array<[TriggerKind, string]> = [
['file', '+ Folder'],
['web', '+ Page'],
['agent', '+ Agent check'],
['custom', '+ Custom'],
+ ['stream', '+ Live feed'],
];
function newSource(kind: TriggerKind): EventSourceConfig {
if (kind === 'file') return { kind: 'file', path: '', poll_seconds: 15 };
if (kind === 'web') return { kind: 'web', url: '', watch_for: '', poll_seconds: 300 };
if (kind === 'agent') return { kind: 'agent', check: '', model: '', poll_seconds: 900 };
+ if (kind === 'stream') return { kind: 'stream', url: '', contains: '' };
return { kind: 'custom' };
}
diff --git a/frontend/src/shared/state/store.ts b/frontend/src/shared/state/store.ts
index 5311d7bc..9ecbfa72 100644
--- a/frontend/src/shared/state/store.ts
+++ b/frontend/src/shared/state/store.ts
@@ -18,6 +18,7 @@ import subscriptionsReducer from './subscriptionsSlice';
import workflowsReducer from './workflowsSlice';
import missedRunsReducer from './missedRunsSlice';
import patternsReducer from './patternsSlice';
+import triggersHealthReducer from './triggersHealthSlice';
import onboardingProgressReducer from '@/shared/state/onboardingProgressSlice';
import onboardingV3Reducer from '@/shared/state/onboardingV3Slice';
@@ -42,6 +43,7 @@ export const store = configureStore({
workflows: workflowsReducer,
missedRuns: missedRunsReducer,
patterns: patternsReducer,
+ triggersHealth: triggersHealthReducer,
onboardingProgress: onboardingProgressReducer,
onboardingV3: onboardingV3Reducer,
},
diff --git a/frontend/src/shared/state/triggersHealthSlice.ts b/frontend/src/shared/state/triggersHealthSlice.ts
new file mode 100644
index 00000000..66952cc2
--- /dev/null
+++ b/frontend/src/shared/state/triggersHealthSlice.ts
@@ -0,0 +1,45 @@
+import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
+import { API_BASE } from '@/shared/config';
+
+export interface TriggerAttentionItem {
+ workflow_id: string;
+ workflow_title: string;
+ trigger_id: string;
+ kind: string;
+ consecutive_failures: number;
+ last_error: string;
+}
+
+interface TriggersHealthState {
+ items: TriggerAttentionItem[];
+ toastOpen: boolean;
+}
+
+const initialState: TriggersHealthState = { items: [], toastOpen: false };
+
+export const fetchTriggersAttention = createAsyncThunk(
+ 'triggersHealth/fetch',
+ async (): Promise<{ attention: TriggerAttentionItem[] }> => {
+ const r = await fetch(`${API_BASE}/workflows/triggers/attention`);
+ return (await r.json()) as { attention: TriggerAttentionItem[] };
+ },
+);
+
+const triggersHealthSlice = createSlice({
+ name: 'triggersHealth',
+ initialState,
+ reducers: {
+ hideTriggersHealthToast(state) {
+ state.toastOpen = false;
+ },
+ },
+ extraReducers: (builder) => {
+ builder.addCase(fetchTriggersAttention.fulfilled, (state, action) => {
+ state.items = action.payload.attention ?? [];
+ state.toastOpen = state.items.length > 0;
+ });
+ },
+});
+
+export const { hideTriggersHealthToast } = triggersHealthSlice.actions;
+export default triggersHealthSlice.reducer;
diff --git a/frontend/src/shared/state/workflowsSlice.ts b/frontend/src/shared/state/workflowsSlice.ts
index e21d4804..c45337b3 100644
--- a/frontend/src/shared/state/workflowsSlice.ts
+++ b/frontend/src/shared/state/workflowsSlice.ts
@@ -56,7 +56,15 @@ export interface CustomEventSource {
kind: 'custom';
}
-export type EventSourceConfig = FileWatchSource | WebWatchSource | AgentCheckSource | CustomEventSource;
+export interface StreamSource {
+ /** Held-open SSE subscription: the source's own event log, read live. */
+ kind: 'stream';
+ url: string;
+ /** Only messages containing this substring become events; empty = everything. */
+ contains: string;
+}
+
+export type EventSourceConfig = FileWatchSource | WebWatchSource | AgentCheckSource | CustomEventSource | StreamSource;
export interface EventTriggerConfig {
id: string;
@@ -177,7 +185,7 @@ export interface WorkflowRun {
session_id: string | null;
error: string | null;
cost_usd: number;
- triggered_by: 'schedule' | 'manual' | 'retry';
+ triggered_by: 'schedule' | 'manual' | 'retry' | 'event';
/** Live "what's the agent doing" subtitle while status is 'running'. */
last_tool_label?: string | null;
/** Currently-executing 0-based step index while status is 'running';