diff --git a/apps/arc-web/app/lib/time.ts b/apps/arc-web/app/lib/time.ts
index cddcd4b16..631b18c71 100644
--- a/apps/arc-web/app/lib/time.ts
+++ b/apps/arc-web/app/lib/time.ts
@@ -12,6 +12,20 @@ export function timeAgo(iso: string): string {
return `${days}d ago`;
}
+/**
+ * Format an ISO 8601 timestamp as a relative future time string (e.g. "in 2h", "in 3d").
+ */
+export function timeUntil(iso: string): string {
+ const seconds = Math.floor((new Date(iso).getTime() - Date.now()) / 1000);
+ if (seconds < 60) return "in <1m";
+ const minutes = Math.floor(seconds / 60);
+ if (minutes < 60) return `in ${minutes}m`;
+ const hours = Math.floor(minutes / 60);
+ if (hours < 24) return `in ${hours}h`;
+ const days = Math.floor(hours / 24);
+ return `in ${days}d`;
+}
+
/**
* Return a human-readable date label for grouping (e.g. "Today", "Yesterday", "Previous 7 days").
*/
diff --git a/apps/arc-web/app/routes/insights.tsx b/apps/arc-web/app/routes/insights.tsx
index d9dfbf0f5..ab05b3274 100644
--- a/apps/arc-web/app/routes/insights.tsx
+++ b/apps/arc-web/app/routes/insights.tsx
@@ -1,6 +1,7 @@
import { Link, Outlet, useNavigate } from "react-router";
import { PlusIcon } from "@heroicons/react/24/outline";
import { apiJson } from "../api-client";
+import { timeAgo } from "../lib/time";
import type { PaginatedSavedQueryList, PaginatedHistoryEntryList } from "@qltysh/arc-api-client";
import type { Route } from "./+types/insights";
@@ -108,7 +109,7 @@ export default function InsightsLayout({ loaderData }: Route.ComponentProps) {
{entry.sql}
- {entry.timestamp} · {entry.rowsReturned} rows
+ {timeAgo(entry.timestamp)} · {entry.rowsReturned} rows
))}
diff --git a/apps/arc-web/app/routes/verification-detail.tsx b/apps/arc-web/app/routes/verification-detail.tsx
index 2a40cdded..0c74f28e4 100644
--- a/apps/arc-web/app/routes/verification-detail.tsx
+++ b/apps/arc-web/app/routes/verification-detail.tsx
@@ -56,6 +56,7 @@ import type {
VerificationStatus,
} from "../data/verifications";
import { apiJson } from "../api-client";
+import { timeAgo } from "../lib/time";
import type { VerificationDetailResponse } from "@qltysh/arc-api-client";
import type { Route } from "./+types/verification-detail";
@@ -317,7 +318,7 @@ export default function VerificationDetail({ loaderData }: Route.ComponentProps)
{run.workflow} |
- {run.timestamp} |
+ {timeAgo(run.timestamp)} |
))}
diff --git a/apps/arc-web/app/routes/workflows.tsx b/apps/arc-web/app/routes/workflows.tsx
index cd48a0ddb..e9a121b8c 100644
--- a/apps/arc-web/app/routes/workflows.tsx
+++ b/apps/arc-web/app/routes/workflows.tsx
@@ -14,6 +14,7 @@ import {
} from "@heroicons/react/24/outline";
import { Link } from "react-router";
import { apiJson } from "../api-client";
+import { timeAgo, timeUntil } from "../lib/time";
import type { PaginatedWorkflowList } from "@qltysh/arc-api-client";
import type { Route } from "./+types/workflows";
@@ -110,10 +111,10 @@ export async function loader({ request }: Route.LoaderArgs) {
name: w.name,
slug: w.slug,
filename: w.filename,
- lastRun: w.last_run?.label ?? "never",
+ lastRun: w.last_run?.ran_at ? timeAgo(w.last_run.ran_at) : "never",
color: slugColorMap[w.slug] ?? "var(--color-teal-500)",
schedule: w.schedule?.expression,
- nextRun: w.schedule?.next_run,
+ nextRun: w.schedule?.next_run ? timeUntil(w.schedule.next_run) : undefined,
}));
return { workflows };
}
diff --git a/crates/arc-api/src/demo/mod.rs b/crates/arc-api/src/demo/mod.rs
index b48d558cb..0c2786125 100644
--- a/crates/arc-api/src/demo/mod.rs
+++ b/crates/arc-api/src/demo/mod.rs
@@ -1104,6 +1104,7 @@ mod usage {
}
mod workflows {
+ use super::ts;
use arc_types::*;
pub fn list_items() -> Vec {
@@ -1112,28 +1113,28 @@ mod workflows {
name: "Fix Build".into(),
slug: "fix_build".into(),
filename: "fix_build.dot".into(),
- last_run: Some(WorkflowLastRun { label: "2 hours ago".into() }),
+ last_run: Some(WorkflowLastRun { ran_at: ts("2025-09-15T12:00:00Z") }),
schedule: None,
},
WorkflowListItem {
name: "Implement Feature".into(),
slug: "implement".into(),
filename: "implement.dot".into(),
- last_run: Some(WorkflowLastRun { label: "4 days ago".into() }),
+ last_run: Some(WorkflowLastRun { ran_at: ts("2025-09-11T10:00:00Z") }),
schedule: None,
},
WorkflowListItem {
name: "Sync Drift".into(),
slug: "sync_drift".into(),
filename: "sync_drift.dot".into(),
- last_run: Some(WorkflowLastRun { label: "1 day ago".into() }),
+ last_run: Some(WorkflowLastRun { ran_at: ts("2025-09-14T14:00:00Z") }),
schedule: None,
},
WorkflowListItem {
name: "Expand Product".into(),
slug: "expand".into(),
filename: "expand.dot".into(),
- last_run: Some(WorkflowLastRun { label: "2 weeks ago".into() }),
+ last_run: Some(WorkflowLastRun { ran_at: ts("2025-09-01T08:00:00Z") }),
schedule: None,
},
]
@@ -1373,6 +1374,7 @@ disk = 10
}
mod verifications {
+ use super::ts;
use arc_types::*;
// ── Category definitions (name, question, controls) ─────────────────
@@ -1417,35 +1419,35 @@ mod verifications {
run_title: "PR #312 \u{2014} Add OAuth2 PKCE flow",
workflow: "code_review",
result: VerificationStatus::Pass,
- timestamp: "2h ago",
+ timestamp: "2025-09-15T12:00:00Z",
},
RecentResultDef {
run_id: "run-046",
run_title: "PR #311 \u{2014} Update rate limiter config",
workflow: "code_review",
result: VerificationStatus::Pass,
- timestamp: "5h ago",
+ timestamp: "2025-09-15T09:00:00Z",
},
RecentResultDef {
run_id: "run-044",
run_title: "PR #309 \u{2014} Migrate to pnpm",
workflow: "code_review",
result: VerificationStatus::Pass,
- timestamp: "1d ago",
+ timestamp: "2025-09-14T14:00:00Z",
},
RecentResultDef {
run_id: "run-042",
run_title: "PR #307 \u{2014} Fix session timeout",
workflow: "fix_build",
result: VerificationStatus::Pass,
- timestamp: "2d ago",
+ timestamp: "2025-09-13T14:00:00Z",
},
RecentResultDef {
run_id: "run-040",
run_title: "PR #305 \u{2014} Add webhook retries",
workflow: "code_review",
result: VerificationStatus::Pass,
- timestamp: "3d ago",
+ timestamp: "2025-09-12T14:00:00Z",
},
];
@@ -1455,35 +1457,35 @@ mod verifications {
run_title: "PR #312 \u{2014} Add OAuth2 PKCE flow",
workflow: "code_review",
result: VerificationStatus::Pass,
- timestamp: "2h ago",
+ timestamp: "2025-09-15T12:00:00Z",
},
RecentResultDef {
run_id: "run-046",
run_title: "PR #311 \u{2014} Update rate limiter config",
workflow: "code_review",
result: VerificationStatus::Pass,
- timestamp: "5h ago",
+ timestamp: "2025-09-15T09:00:00Z",
},
RecentResultDef {
run_id: "run-044",
run_title: "PR #309 \u{2014} Migrate to pnpm",
workflow: "code_review",
result: VerificationStatus::Fail,
- timestamp: "1d ago",
+ timestamp: "2025-09-14T14:00:00Z",
},
RecentResultDef {
run_id: "run-042",
run_title: "PR #307 \u{2014} Fix session timeout",
workflow: "fix_build",
result: VerificationStatus::Pass,
- timestamp: "2d ago",
+ timestamp: "2025-09-13T14:00:00Z",
},
RecentResultDef {
run_id: "run-040",
run_title: "PR #305 \u{2014} Add webhook retries",
workflow: "code_review",
result: VerificationStatus::Pass,
- timestamp: "3d ago",
+ timestamp: "2025-09-12T14:00:00Z",
},
];
@@ -1493,35 +1495,35 @@ mod verifications {
run_title: "PR #312 \u{2014} Add OAuth2 PKCE flow",
workflow: "code_review",
result: VerificationStatus::Pass,
- timestamp: "2h ago",
+ timestamp: "2025-09-15T12:00:00Z",
},
RecentResultDef {
run_id: "run-046",
run_title: "PR #311 \u{2014} Update rate limiter config",
workflow: "code_review",
result: VerificationStatus::Fail,
- timestamp: "5h ago",
+ timestamp: "2025-09-15T09:00:00Z",
},
RecentResultDef {
run_id: "run-044",
run_title: "PR #309 \u{2014} Migrate to pnpm",
workflow: "code_review",
result: VerificationStatus::Pass,
- timestamp: "1d ago",
+ timestamp: "2025-09-14T14:00:00Z",
},
RecentResultDef {
run_id: "run-042",
run_title: "PR #307 \u{2014} Fix session timeout",
workflow: "fix_build",
result: VerificationStatus::Pass,
- timestamp: "2d ago",
+ timestamp: "2025-09-13T14:00:00Z",
},
RecentResultDef {
run_id: "run-040",
run_title: "PR #305 \u{2014} Add webhook retries",
workflow: "code_review",
result: VerificationStatus::Fail,
- timestamp: "3d ago",
+ timestamp: "2025-09-12T14:00:00Z",
},
];
@@ -1531,35 +1533,35 @@ mod verifications {
run_title: "PR #312 \u{2014} Add OAuth2 PKCE flow",
workflow: "code_review",
result: VerificationStatus::Fail,
- timestamp: "2h ago",
+ timestamp: "2025-09-15T12:00:00Z",
},
RecentResultDef {
run_id: "run-046",
run_title: "PR #311 \u{2014} Update rate limiter config",
workflow: "code_review",
result: VerificationStatus::Pass,
- timestamp: "5h ago",
+ timestamp: "2025-09-15T09:00:00Z",
},
RecentResultDef {
run_id: "run-044",
run_title: "PR #309 \u{2014} Migrate to pnpm",
workflow: "code_review",
result: VerificationStatus::Fail,
- timestamp: "1d ago",
+ timestamp: "2025-09-14T14:00:00Z",
},
RecentResultDef {
run_id: "run-042",
run_title: "PR #307 \u{2014} Fix session timeout",
workflow: "fix_build",
result: VerificationStatus::Fail,
- timestamp: "2d ago",
+ timestamp: "2025-09-13T14:00:00Z",
},
RecentResultDef {
run_id: "run-040",
run_title: "PR #305 \u{2014} Add webhook retries",
workflow: "code_review",
result: VerificationStatus::Pass,
- timestamp: "3d ago",
+ timestamp: "2025-09-12T14:00:00Z",
},
];
@@ -2027,7 +2029,7 @@ mod verifications {
run: RunReference { id: r.run_id.into(), title: r.run_title.into() },
workflow: WorkflowReference { slug: r.workflow.into() },
result: r.result,
- timestamp: r.timestamp.into(),
+ timestamp: ts(r.timestamp),
})
.collect()
}
@@ -2420,14 +2422,14 @@ mod insights {
HistoryEntry {
id: "h1".into(),
sql: "SELECT workflow_name, COUNT(*) FROM runs GROUP BY 1".into(),
- timestamp: "2 min ago".into(),
+ timestamp: ts("2025-09-15T13:58:00Z"),
elapsed: 0.342,
row_count: 6,
},
HistoryEntry {
id: "h2".into(),
sql: "SELECT * FROM runs WHERE status = 'failed' LIMIT 100".into(),
- timestamp: "8 min ago".into(),
+ timestamp: ts("2025-09-15T13:52:00Z"),
elapsed: 0.127,
row_count: 23,
},
@@ -2435,7 +2437,7 @@ mod insights {
id: "h3".into(),
sql: "SELECT date_trunc('day', created_at) as d, COUNT(*) FROM runs GROUP BY 1"
.into(),
- timestamp: "15 min ago".into(),
+ timestamp: ts("2025-09-15T13:45:00Z"),
elapsed: 0.531,
row_count: 30,
},
diff --git a/docs/api-reference/arc-api.yaml b/docs/api-reference/arc-api.yaml
index f25d8e31c..ae8a62556 100644
--- a/docs/api-reference/arc-api.yaml
+++ b/docs/api-reference/arc-api.yaml
@@ -1796,19 +1796,21 @@ components:
example: "0 */6 * * *"
next_run:
type: string
- description: Human-readable relative timestamp of the next run.
- example: in 4 hours
+ format: date-time
+ description: ISO 8601 timestamp of the next scheduled run.
+ example: "2025-09-15T18:00:00Z"
WorkflowLastRun:
description: Information about a workflow's most recent run.
type: object
required:
- - label
+ - ran_at
properties:
- label:
+ ran_at:
type: string
- description: Human-readable relative timestamp.
- example: 2 hours ago
+ format: date-time
+ description: ISO 8601 timestamp of the most recent run.
+ example: "2025-09-15T12:00:00Z"
UsageStageRef:
description: Reference to a usage stage.
@@ -2536,8 +2538,9 @@ components:
$ref: "#/components/schemas/VerificationStatus"
timestamp:
type: string
- description: Human-readable relative timestamp of the evaluation.
- example: 2h ago
+ format: date-time
+ description: ISO 8601 timestamp of the evaluation.
+ example: "2025-09-15T12:00:00Z"
SiblingControl:
description: Summary of a sibling verification control in the same category.
@@ -2998,8 +3001,9 @@ components:
example: "SELECT workflow_name, COUNT(*) FROM runs GROUP BY 1"
timestamp:
type: string
- description: Human-readable relative timestamp of execution.
- example: 2 min ago
+ format: date-time
+ description: ISO 8601 timestamp of execution.
+ example: "2025-09-15T14:00:00Z"
elapsed:
type: number
description: Query execution time in seconds.
diff --git a/packages/arc-api-client/src/models/history-entry.ts b/packages/arc-api-client/src/models/history-entry.ts
index a002be31f..b6277f1f4 100644
--- a/packages/arc-api-client/src/models/history-entry.ts
+++ b/packages/arc-api-client/src/models/history-entry.ts
@@ -27,7 +27,7 @@ export interface HistoryEntry {
*/
'sql': string;
/**
- * Human-readable relative timestamp of execution.
+ * ISO 8601 timestamp of execution.
*/
'timestamp': string;
/**
diff --git a/packages/arc-api-client/src/models/recent-control-result.ts b/packages/arc-api-client/src/models/recent-control-result.ts
index e1d3966c8..cc2ad2e63 100644
--- a/packages/arc-api-client/src/models/recent-control-result.ts
+++ b/packages/arc-api-client/src/models/recent-control-result.ts
@@ -31,7 +31,7 @@ export interface RecentControlResult {
'workflow': WorkflowReference;
'result': VerificationStatus;
/**
- * Human-readable relative timestamp of the evaluation.
+ * ISO 8601 timestamp of the evaluation.
*/
'timestamp': string;
}
diff --git a/packages/arc-api-client/src/models/workflow-last-run.ts b/packages/arc-api-client/src/models/workflow-last-run.ts
index 0f40c1912..0cf0887ba 100644
--- a/packages/arc-api-client/src/models/workflow-last-run.ts
+++ b/packages/arc-api-client/src/models/workflow-last-run.ts
@@ -19,8 +19,8 @@
*/
export interface WorkflowLastRun {
/**
- * Human-readable relative timestamp.
+ * ISO 8601 timestamp of the most recent run.
*/
- 'label': string;
+ 'ran_at': string;
}
diff --git a/packages/arc-api-client/src/models/workflow-schedule.ts b/packages/arc-api-client/src/models/workflow-schedule.ts
index 2df677374..cd9266038 100644
--- a/packages/arc-api-client/src/models/workflow-schedule.ts
+++ b/packages/arc-api-client/src/models/workflow-schedule.ts
@@ -23,7 +23,7 @@ export interface WorkflowSchedule {
*/
'expression': string;
/**
- * Human-readable relative timestamp of the next run.
+ * ISO 8601 timestamp of the next scheduled run.
*/
'next_run'?: string;
}