diff --git a/ui/src/components/workflows/WorkflowForm.tsx b/ui/src/components/workflows/WorkflowForm.tsx
index 505b532b..8026d747 100644
--- a/ui/src/components/workflows/WorkflowForm.tsx
+++ b/ui/src/components/workflows/WorkflowForm.tsx
@@ -96,7 +96,7 @@ export function WorkflowForm({
if (workflow) {
setName(workflow.name);
setAgentId(workflow.agent_id);
- const src = workflow.source_config;
+ const src = workflow.trigger_config;
if (src?.type === "github_issues") {
setOwner(src.owner);
setRepo(src.repo);
@@ -168,7 +168,7 @@ export function WorkflowForm({
const request: CreateWorkflowRequest = {
name: name.trim(),
agent_id: agentId,
- source_config: {
+ trigger_config: {
type: "github_issues",
owner: owner.trim(),
repo: repo.trim(),
diff --git a/ui/src/components/workflows/WorkflowTable.tsx b/ui/src/components/workflows/WorkflowTable.tsx
index 8cf0f944..5a0885c8 100644
--- a/ui/src/components/workflows/WorkflowTable.tsx
+++ b/ui/src/components/workflows/WorkflowTable.tsx
@@ -35,7 +35,7 @@ export interface WorkflowTableProps {
// ---------------------------------------------------------------------------
function sourceLabel(workflow: Workflow): string {
- const src = workflow.source_config;
+ const src = workflow.trigger_config;
if (!src) return "No source";
if (src.type === "github_issues") {
const parts = [`${src.owner}/${src.repo}`];
diff --git a/ui/src/pages/workflows/WorkflowDetail.tsx b/ui/src/pages/workflows/WorkflowDetail.tsx
index 08484d48..438a96bb 100644
--- a/ui/src/pages/workflows/WorkflowDetail.tsx
+++ b/ui/src/pages/workflows/WorkflowDetail.tsx
@@ -18,33 +18,54 @@ import { DispatchHistory } from "@/components/workflows/DispatchHistory";
import { WorkflowForm } from "@/components/workflows/WorkflowForm";
import { useAgents } from "@/hooks/useAgents";
import { useWorkflowDetail } from "@/hooks/useWorkflows";
-import type { CreateWorkflowRequest } from "@/types/orchestrator";
+import type { CreateWorkflowRequest, TriggerConfig } from "@/types/orchestrator";
+import { getTriggerLabel } from "@/types/orchestrator";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
-function sourceDetail(
- src:
- | {
- type: string;
- owner?: string;
- repo?: string;
- labels?: string[];
- state?: string;
- }
- | undefined,
-): string {
+function sourceDetail(src: TriggerConfig | undefined): string {
if (!src) return "No source configured";
- if (src.type === "github_issues") {
- const parts: string[] = [];
- if (src.owner && src.repo) parts.push(`${src.owner}/${src.repo}`);
- if (src.labels && src.labels.length > 0)
- parts.push(`Labels: ${src.labels.join(", ")}`);
- if (src.state) parts.push(`State: ${src.state}`);
- return parts.join(" · ");
+ switch (src.type) {
+ case "github_issues":
+ case "github_pull_requests": {
+ const parts: string[] = [`${src.owner}/${src.repo}`];
+ if (src.labels.length > 0) parts.push(`Labels: ${src.labels.join(", ")}`);
+ if (src.state) parts.push(`State: ${src.state}`);
+ return parts.join(" · ");
+ }
+ case "cron":
+ return `Cron: ${src.expression}`;
+ case "delay":
+ return `Run at: ${new Date(src.run_at).toLocaleString()}`;
+ case "webhook":
+ return `Webhook (${src.source})`;
+ case "manual":
+ return "Manual trigger";
+ case "linear_issues": {
+ const parts: string[] = [];
+ if (src.team_key) parts.push(src.team_key);
+ if (src.project) parts.push(src.project);
+ return parts.length > 0 ? `Linear: ${parts.join(" / ")}` : "Linear Issues";
+ }
+ case "agent_lifecycle":
+ return `Agent lifecycle: ${src.event}`;
+ case "agent_idle":
+ return `Agent idle: ${src.idle_seconds}s`;
+ case "dispatch_result":
+ return src.source_workflow_id
+ ? `Dispatch result from ${src.source_workflow_id}`
+ : "Dispatch result";
+ case "composite":
+ return `Composite (${src.mode.toUpperCase()}, ${src.triggers.length} triggers)`;
+ case "queue":
+ return `Queue: ${src.queue_name}`;
+ case "ask_response":
+ return src.category ? `Ask response: ${src.category}` : "Ask response";
+ default:
+ return getTriggerLabel((src as TriggerConfig).type);
}
- return src.type;
}
function formatDateTime(iso: string): string {
@@ -211,7 +232,7 @@ export function WorkflowDetail() {
{
});
});
- describe("edit mode — valid source_config", () => {
+ describe("edit mode — valid trigger_config", () => {
it("populates the name field", async () => {
renderForm(validWorkflow);
await waitFor(() => {
@@ -143,21 +143,21 @@ describe("WorkflowForm", () => {
});
});
- describe("edit mode — missing source_config (null guard)", () => {
- it("does not throw when source_config is undefined", () => {
+ describe("edit mode — missing trigger_config (null guard)", () => {
+ it("does not throw when trigger_config is undefined", () => {
// Cast to bypass TypeScript: simulates an API response with missing field
const brokenWorkflow = {
...validWorkflow,
- source_config: undefined,
+ trigger_config: undefined,
} as unknown as Workflow;
expect(() => renderForm(brokenWorkflow)).not.toThrow();
});
- it("renders the dialog without crashing when source_config is undefined", () => {
+ it("renders the dialog without crashing when trigger_config is undefined", () => {
const brokenWorkflow = {
...validWorkflow,
- source_config: undefined,
+ trigger_config: undefined,
} as unknown as Workflow;
renderForm(brokenWorkflow);
@@ -167,10 +167,10 @@ describe("WorkflowForm", () => {
).toBeInTheDocument();
});
- it("leaves GitHub fields at defaults when source_config is undefined", async () => {
+ it("leaves GitHub fields at defaults when trigger_config is undefined", async () => {
const brokenWorkflow = {
...validWorkflow,
- source_config: undefined,
+ trigger_config: undefined,
} as unknown as Workflow;
renderForm(brokenWorkflow);
@@ -182,10 +182,10 @@ describe("WorkflowForm", () => {
});
});
- it("does not throw when source_config is null", () => {
+ it("does not throw when trigger_config is null", () => {
const brokenWorkflow = {
...validWorkflow,
- source_config: null,
+ trigger_config: null,
} as unknown as Workflow;
expect(() => renderForm(brokenWorkflow)).not.toThrow();
diff --git a/ui/src/test/hooks/useWorkflows.test.ts b/ui/src/test/hooks/useWorkflows.test.ts
index 6ed4e598..9047d8ce 100644
--- a/ui/src/test/hooks/useWorkflows.test.ts
+++ b/ui/src/test/hooks/useWorkflows.test.ts
@@ -17,7 +17,7 @@ function makeWorkflow(overrides: Partial = {}): Workflow {
id: "wf-1",
name: "Test Workflow",
agent_id: "agent-1",
- source_config: {
+ trigger_config: {
type: "github_issues",
owner: "acme",
repo: "myrepo",
diff --git a/ui/src/types/orchestrator.ts b/ui/src/types/orchestrator.ts
index 558e8621..23696042 100644
--- a/ui/src/types/orchestrator.ts
+++ b/ui/src/types/orchestrator.ts
@@ -166,10 +166,15 @@ export type DispatchStatus =
| "failed"
| "skipped";
+// ---------------------------------------------------------------------------
+// TriggerConfig — covers all 14 backend trigger variants
+// ---------------------------------------------------------------------------
+
/**
- * Tagged union for different task source backends.
+ * Tagged union for all workflow trigger types.
+ * Mirrors the Rust `TriggerConfig` enum in crates/orchestrator/src/scheduler/types.rs.
*/
-export type TaskSourceConfig =
+export type TriggerConfig =
| {
type: "github_issues";
owner: string;
@@ -183,8 +188,137 @@ export type TaskSourceConfig =
repo: string;
labels: string[];
state: "open" | "closed" | "merged" | "all";
+ }
+ | { type: "cron"; expression: string }
+ | { type: "delay"; run_at: string }
+ | {
+ type: "webhook";
+ secret?: string;
+ source: "github" | "linear" | "any";
+ }
+ | { type: "manual" }
+ | {
+ type: "linear_issues";
+ team_key?: string;
+ project?: string;
+ status?: string[];
+ labels: string[];
+ assignee?: string;
+ }
+ | {
+ type: "agent_lifecycle";
+ event: "session_start" | "session_end" | "context_clear";
+ }
+ | { type: "agent_idle"; idle_seconds: number }
+ | {
+ type: "dispatch_result";
+ source_workflow_id?: string;
+ status?: DispatchStatus;
+ }
+ | {
+ type: "composite";
+ mode: "or" | "and";
+ triggers: TriggerConfig[];
+ correlation_window_secs?: number;
+ }
+ | {
+ type: "queue";
+ queue_name: string;
+ poll_interval_secs?: number;
+ visibility_timeout_secs?: number;
+ }
+ | {
+ type: "ask_response";
+ agent_id?: string;
+ category?: string;
+ response_pattern?: string;
};
+/** Convenience union of all trigger type string literals */
+export type TriggerType = TriggerConfig["type"];
+
+/** Logical grouping of trigger types */
+export type TriggerCategory = "external" | "schedule" | "event" | "internal";
+
+// ---------------------------------------------------------------------------
+// TriggerConfig helpers
+// ---------------------------------------------------------------------------
+
+/** Human-readable label for a trigger type */
+export function getTriggerLabel(type: TriggerType): string {
+ const labels: Record = {
+ github_issues: "GitHub Issues",
+ github_pull_requests: "GitHub Pull Requests",
+ cron: "Cron Schedule",
+ delay: "Delayed Run",
+ webhook: "Webhook",
+ manual: "Manual",
+ linear_issues: "Linear Issues",
+ agent_lifecycle: "Agent Lifecycle",
+ agent_idle: "Agent Idle",
+ dispatch_result: "Dispatch Result",
+ composite: "Composite",
+ queue: "Queue",
+ ask_response: "Ask Response",
+ };
+ return labels[type] ?? type;
+}
+
+/** Logical category a trigger type belongs to */
+export function getTriggerCategory(type: TriggerType): TriggerCategory {
+ const categories: Record = {
+ github_issues: "external",
+ github_pull_requests: "external",
+ linear_issues: "external",
+ webhook: "external",
+ cron: "schedule",
+ delay: "schedule",
+ agent_lifecycle: "event",
+ agent_idle: "event",
+ dispatch_result: "event",
+ ask_response: "event",
+ manual: "internal",
+ queue: "internal",
+ composite: "internal",
+ };
+ return categories[type] ?? "internal";
+}
+
+/** Default (empty/safe) TriggerConfig for a given type */
+export function getDefaultTriggerConfig(type: TriggerType): TriggerConfig {
+ switch (type) {
+ case "github_issues":
+ return { type, owner: "", repo: "", labels: [], state: "open" };
+ case "github_pull_requests":
+ return { type, owner: "", repo: "", labels: [], state: "open" };
+ case "cron":
+ return { type, expression: "0 * * * *" };
+ case "delay":
+ return { type, run_at: new Date().toISOString() };
+ case "webhook":
+ return { type, source: "any" };
+ case "manual":
+ return { type };
+ case "linear_issues":
+ return { type, labels: [] };
+ case "agent_lifecycle":
+ return { type, event: "session_start" };
+ case "agent_idle":
+ return { type, idle_seconds: 300 };
+ case "dispatch_result":
+ return { type };
+ case "composite":
+ return { type, mode: "or", triggers: [] };
+ case "queue":
+ return { type, queue_name: "" };
+ case "ask_response":
+ return { type };
+ }
+}
+
+/** @deprecated Use TriggerConfig instead */
+export type TaskSourceConfig = TriggerConfig;
+
/**
* A workflow as returned by the API.
* Mirrors the Rust WorkflowResponse type.
@@ -193,7 +327,7 @@ export interface Workflow {
id: string;
name: string;
agent_id: string;
- source_config: TaskSourceConfig;
+ trigger_config: TriggerConfig;
prompt_template: string;
poll_interval_secs: number;
enabled: boolean;
@@ -235,7 +369,7 @@ export interface Task {
export interface CreateWorkflowRequest {
name: string;
agent_id: string;
- source_config: TaskSourceConfig;
+ trigger_config: TriggerConfig;
prompt_template: string;
poll_interval_secs: number;
enabled: boolean;