66// stock reads `vendor/intx/hub-api/src/routes/workflows.ts` exposes.
77//
88// The `schedule` trigger is reserved on Interchange but unimplemented — no
9- // scheduler fires it — so this reads deployments as plain workflows, with
10- // no schedule concept. Run-now and pause/resume have
11- // no backing stock route either (`/deployments` is list/create only; no
12- // per-deployment PATCH or trigger route exists), so both stay rejected
13- // promises with a message naming the missing route, same pattern as before.
9+ // scheduler fires it — so a schedule here is a `@corbits/cron` row addressed
10+ // at the deployment's live run, joined in from `GET /cron` by that address
11+ // (the same `run_<id>@<domain>` join `chat/threads-api.ts` does against
12+ // `listTopLevelRuns`). Run-now and pause/resume have no backing stock route
13+ // either (`/deployments` is list/create only; no per-deployment PATCH or
14+ // trigger route exists), so both stay rejected promises with a message
15+ // naming the missing route, same pattern as before.
1416
1517import { type } from "arktype" ;
1618import { useQuery } from "@tanstack/react-query" ;
1719import { WorkflowDeploymentResponse } from "@intx/types" ;
1820import type { APIQuery } from "@/lib/api-query" ;
1921import { ApiQueryError , UnauthenticatedError , toAPIQuery } from "@/lib/api-query" ;
2022import { isAgentDeploySourceAssetName } from "@/agent-deploy" ;
23+ import { listTopLevelRuns } from "@/agents-api" ;
2124import { MYRA_SOURCE_CONFIG } from "@/myra-source" ;
2225
2326export const ScheduledWorkflowDefinition = type ( {
@@ -28,10 +31,27 @@ export const ScheduledWorkflowDefinition = type({
2831 status : "'deployed' | 'stopped'" ,
2932 createdAt : "string" ,
3033 updatedAt : "string" ,
34+ /** The cron expression firing this deployment's live run, or null when no
35+ * `@corbits/cron` row is addressed at it. */
36+ schedule : "string | null" ,
3137} ) ;
3238
3339export type ScheduledWorkflowDefinition = typeof ScheduledWorkflowDefinition . infer ;
3440
41+ export const CronSchedule = type ( {
42+ id : "string" ,
43+ tenantId : "string" ,
44+ expression : "string" ,
45+ toAddress : "string" ,
46+ subject : "string" ,
47+ body : "string" ,
48+ createdAt : "string" ,
49+ } ) ;
50+
51+ export type CronSchedule = typeof CronSchedule . infer ;
52+
53+ const CronSchedulesResponse = type ( { schedules : CronSchedule . array ( ) } ) ;
54+
3555const DeploymentsSchema = WorkflowDeploymentResponse . array ( ) ;
3656const WorkflowAssetSchema = type ( { id : "string" , name : "string" } ) ;
3757const WorkflowAssetsSchema = WorkflowAssetSchema . array ( ) ;
@@ -44,6 +64,16 @@ function workflowAssetsPath(tenantId: string): string {
4464 return `/api/tenants/${ tenantId } /assets?kind=workflow&inherited=false` ;
4565}
4666
67+ function cronPath ( tenantId : string ) : string {
68+ return `/api/tenants/${ tenantId } /cron` ;
69+ }
70+
71+ /** Every cron schedule saved on this tenant. */
72+ export async function listCronSchedules ( tenantId : string ) : Promise < readonly CronSchedule [ ] > {
73+ const parsed = await fetchJSON ( cronPath ( tenantId ) , CronSchedulesResponse ) ;
74+ return parsed . schedules ;
75+ }
76+
4777async function fetchJSON < T > ( path : string , schema : ( data : unknown ) => T | type . errors ) : Promise < T > {
4878 const response = await fetch ( path , { headers : { accept : "application/json" } } ) ;
4979 if ( response . status === 401 ) throw new UnauthenticatedError ( ) ;
@@ -72,25 +102,36 @@ function isAgentAssetName(name: string): boolean {
72102export async function listScheduledWorkflows (
73103 tenantId : string ,
74104) : Promise < readonly ScheduledWorkflowDefinition [ ] > {
75- const [ deployments , assets ] = await Promise . all ( [
105+ const [ deployments , assets , runs , schedules ] = await Promise . all ( [
76106 fetchJSON ( deploymentsPath ( tenantId ) , DeploymentsSchema ) ,
77107 fetchJSON ( workflowAssetsPath ( tenantId ) , WorkflowAssetsSchema ) ,
108+ listTopLevelRuns ( tenantId ) ,
109+ listCronSchedules ( tenantId ) ,
78110 ] ) ;
79111 const nameByAssetId = new Map ( assets . map ( ( asset ) => [ asset . id , asset . name ] ) ) ;
112+ // A deployment's own id is its anchor run's id (see `chat/threads-api.ts`'s
113+ // `listChatAgents`), so this is the same join that resolves a chat agent's
114+ // live address.
115+ const addressByRunId = new Map ( runs . map ( ( run ) => [ run . id , run . address ] ) ) ;
116+ const expressionByAddress = new Map ( schedules . map ( ( row ) => [ row . toAddress , row . expression ] ) ) ;
80117 return deployments
81118 . filter ( ( deployment ) => {
82119 const name = nameByAssetId . get ( deployment . definitionAssetId ) ;
83120 return name === undefined || ! isAgentAssetName ( name ) ;
84121 } )
85- . map ( ( deployment ) => ( {
86- definitionId : deployment . id ,
87- assetId : deployment . definitionAssetId ,
88- name : nameByAssetId . get ( deployment . definitionAssetId ) ?? "Untitled workflow" ,
89- tenantId : deployment . tenantId ,
90- status : deployment . status === "deployed" ? "deployed" : "stopped" ,
91- createdAt : deployment . createdAt ,
92- updatedAt : deployment . createdAt ,
93- } ) ) ;
122+ . map ( ( deployment ) => {
123+ const address = addressByRunId . get ( deployment . id ) ;
124+ return {
125+ definitionId : deployment . id ,
126+ assetId : deployment . definitionAssetId ,
127+ name : nameByAssetId . get ( deployment . definitionAssetId ) ?? "Untitled workflow" ,
128+ tenantId : deployment . tenantId ,
129+ status : deployment . status === "deployed" ? "deployed" : "stopped" ,
130+ createdAt : deployment . createdAt ,
131+ updatedAt : deployment . createdAt ,
132+ schedule : address === undefined ? null : ( expressionByAddress . get ( address ) ?? null ) ,
133+ } ;
134+ } ) ;
94135}
95136
96137/** No stock route reruns a deployment on demand yet. */
0 commit comments