diff --git a/__tests__/hooks/useFlowRuns.test.tsx b/__tests__/hooks/useFlowRuns.test.tsx
new file mode 100644
index 0000000..9ffa968
--- /dev/null
+++ b/__tests__/hooks/useFlowRuns.test.tsx
@@ -0,0 +1,88 @@
+/**
+ * Tests for useFlowRuns / useTriggerFlow — validates run-history fetch and,
+ * critically, that a 200 response carrying an inner { success: false } envelope
+ * is surfaced as a failure (not a false success).
+ */
+import React from "react";
+import { renderHook, waitFor, act } from "@testing-library/react-native";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+
+/* ---- Mock the SDK client ---- */
+const mockListRuns = jest.fn();
+const mockExecute = jest.fn();
+const mockClient = {
+ automation: { listRuns: mockListRuns, execute: mockExecute, getRun: jest.fn() },
+};
+jest.mock("@objectstack/client-react", () => ({
+ useClient: () => mockClient,
+}));
+
+import { useFlowRuns, useTriggerFlow } from "~/hooks/useFlowRuns";
+
+function wrapper({ children }: { children: React.ReactNode }) {
+ const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
+ return {children};
+}
+
+beforeEach(() => {
+ mockListRuns.mockReset();
+ mockExecute.mockReset();
+});
+
+describe("useFlowRuns", () => {
+ it("returns the run list + hasMore", async () => {
+ mockListRuns.mockResolvedValue({
+ runs: [{ id: "r1", flowName: "f", status: "success" }],
+ hasMore: true,
+ });
+ const { result } = renderHook(() => useFlowRuns("f"), { wrapper });
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+ expect(mockListRuns).toHaveBeenCalledWith("f", { limit: 25 });
+ expect(result.current.data).toEqual({
+ runs: [{ id: "r1", flowName: "f", status: "success" }],
+ hasMore: true,
+ });
+ });
+
+ it("defaults to an empty list when the response is empty", async () => {
+ mockListRuns.mockResolvedValue(undefined);
+ const { result } = renderHook(() => useFlowRuns("f"), { wrapper });
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+ expect(result.current.data).toEqual({ runs: [], hasMore: false });
+ });
+});
+
+describe("useTriggerFlow", () => {
+ it("reports ok on a successful run", async () => {
+ mockExecute.mockResolvedValue({ runId: "r9", status: "success" });
+ const { result } = renderHook(() => useTriggerFlow(), { wrapper });
+ let res: { ok: boolean; error?: string } = { ok: false };
+ await act(async () => {
+ res = await result.current.trigger("f");
+ });
+ expect(res.ok).toBe(true);
+ });
+
+ it("surfaces an inner { success: false } envelope as a failure", async () => {
+ // HTTP 200 but the engine couldn't run the flow.
+ mockExecute.mockResolvedValue({ success: false, error: "Flow 'f' not found" });
+ const { result } = renderHook(() => useTriggerFlow(), { wrapper });
+ let res: { ok: boolean; error?: string } = { ok: true };
+ await act(async () => {
+ res = await result.current.trigger("f");
+ });
+ expect(res.ok).toBe(false);
+ expect(res.error).toBe("Flow 'f' not found");
+ });
+
+ it("reports the error message when the request throws", async () => {
+ mockExecute.mockRejectedValue(new Error("network down"));
+ const { result } = renderHook(() => useTriggerFlow(), { wrapper });
+ let res: { ok: boolean; error?: string } = { ok: true };
+ await act(async () => {
+ res = await result.current.trigger("f");
+ });
+ expect(res.ok).toBe(false);
+ expect(res.error).toBe("network down");
+ });
+});
diff --git a/app/flows/[name].tsx b/app/flows/[name].tsx
deleted file mode 100644
index f1b48e9..0000000
--- a/app/flows/[name].tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import { View, Text, ScrollView } from "react-native";
-import { SafeAreaView } from "react-native-safe-area-context";
-import { useLocalSearchParams } from "expo-router";
-import { Workflow } from "lucide-react-native";
-import { ScreenHeader } from "~/components/common/ScreenHeader";
-import { Badge } from "~/components/ui/Badge";
-import { EmptyState } from "~/components/ui/EmptyState";
-import { ListSkeleton } from "~/components/ui/ListSkeleton";
-import { FlowViewer } from "~/components/automation/FlowViewer";
-import { useFlow } from "~/hooks/useFlows";
-
-function humanize(token: string): string {
- return token.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
-}
-
-/**
- * Flow detail — renders a read-only `FlowViewer` diagram of a single automation
- * flow's nodes and edges, plus its trigger/status metadata.
- */
-export default function FlowDetailScreen() {
- const { name } = useLocalSearchParams<{ name: string }>();
- const flowName = Array.isArray(name) ? name[0] : name;
- const { flow, isLoading, error } = useFlow(flowName);
-
- if (isLoading) {
- return (
-
-
-
-
- );
- }
-
- if (error || !flow) {
- return (
-
-
-
-
- );
- }
-
- const inputs = flow.variables.filter((v) => v.isInput);
-
- return (
-
-
-
- {/* Metadata summary */}
-
- {flow.description ? (
- {flow.description}
- ) : null}
-
- {flow.type ? {humanize(flow.type)} : null}
-
- {flow.status ? humanize(flow.status) : flow.active ? "Active" : "Inactive"}
-
- {typeof flow.version === "number" ? (
- {`v${flow.version}`}
- ) : null}
- {inputs.length > 0 ? (
- {`${inputs.length} input${inputs.length === 1 ? "" : "s"}`}
- ) : null}
-
-
-
- {/* Flow diagram */}
-
-
-
-
- );
-}
diff --git a/app/flows/[name]/index.tsx b/app/flows/[name]/index.tsx
new file mode 100644
index 0000000..19cf79c
--- /dev/null
+++ b/app/flows/[name]/index.tsx
@@ -0,0 +1,154 @@
+import { View, Text, ScrollView, Pressable, ActivityIndicator } from "react-native";
+import { SafeAreaView } from "react-native-safe-area-context";
+import { useLocalSearchParams, useRouter } from "expo-router";
+import { useTranslation } from "react-i18next";
+import { Workflow, Play } from "lucide-react-native";
+import { ScreenHeader } from "~/components/common/ScreenHeader";
+import { Badge } from "~/components/ui/Badge";
+import { EmptyState } from "~/components/ui/EmptyState";
+import { ListSkeleton } from "~/components/ui/ListSkeleton";
+import { useToast } from "~/components/ui/Toast";
+import { useConfirm } from "~/components/ui/ConfirmDialog";
+import { FlowViewer } from "~/components/automation/FlowViewer";
+import { FlowRunList } from "~/components/automation/FlowRunList";
+import { useFlow } from "~/hooks/useFlows";
+import { useFlowRuns, useTriggerFlow } from "~/hooks/useFlowRuns";
+
+function humanize(token: string): string {
+ return token.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
+}
+
+/**
+ * Flow detail — a read-only `FlowViewer` diagram of one automation flow, plus a
+ * "Run" action (manual trigger) and the recent run history.
+ */
+export default function FlowDetailScreen() {
+ const { name } = useLocalSearchParams<{ name: string }>();
+ const flowName = Array.isArray(name) ? name[0] : name;
+ const router = useRouter();
+ const { t } = useTranslation();
+ const { toastSuccess, toastError } = useToast();
+ const confirm = useConfirm();
+
+ const { flow, isLoading, error } = useFlow(flowName);
+ const { data: runsData, isLoading: runsLoading } = useFlowRuns(flowName);
+ const { trigger, isTriggering } = useTriggerFlow();
+
+ const runs = runsData?.runs ?? [];
+
+ const handleRun = async () => {
+ if (!flow) return;
+ const ok = await confirm({
+ title: t("workflow.runFlow"),
+ message: t("workflow.runConfirm", { flow: flow.label }),
+ confirmLabel: t("workflow.runLabel"),
+ });
+ if (!ok) return;
+ const result = await trigger(flow.name);
+ if (result.ok) {
+ toastSuccess(t("workflow.runStarted"));
+ } else {
+ toastError(result.error ?? t("workflow.runFailed"));
+ }
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+
+ );
+ }
+
+ if (error || !flow) {
+ return (
+
+
+
+
+ );
+ }
+
+ const inputs = flow.variables.filter((v) => v.isInput);
+
+ return (
+
+
+ {isTriggering ? (
+
+ ) : (
+
+ )}
+
+ {t("workflow.runLabel")}
+
+
+ }
+ />
+
+ {/* Metadata summary */}
+
+ {flow.description ? (
+ {flow.description}
+ ) : null}
+
+ {flow.type ? {humanize(flow.type)} : null}
+
+ {flow.status ? humanize(flow.status) : flow.active ? "Active" : "Inactive"}
+
+ {typeof flow.version === "number" ? (
+ {`v${flow.version}`}
+ ) : null}
+ {inputs.length > 0 ? (
+ {`${inputs.length} input${inputs.length === 1 ? "" : "s"}`}
+ ) : null}
+
+
+
+
+ {/* Flow diagram */}
+
+
+ {/* Run history */}
+
+
+ {t("workflow.runHistory")}
+
+ {runsLoading ? (
+
+ ) : runs.length === 0 ? (
+ {t("workflow.noRuns")}
+ ) : (
+
+ router.push(
+ `/flows/${encodeURIComponent(flow.name)}/runs/${encodeURIComponent(run.id)}`,
+ )
+ }
+ />
+ )}
+
+
+
+ );
+}
diff --git a/app/flows/[name]/runs/[runId].tsx b/app/flows/[name]/runs/[runId].tsx
new file mode 100644
index 0000000..3d6d18f
--- /dev/null
+++ b/app/flows/[name]/runs/[runId].tsx
@@ -0,0 +1,119 @@
+import { View, Text, ScrollView } from "react-native";
+import { SafeAreaView } from "react-native-safe-area-context";
+import { useLocalSearchParams } from "expo-router";
+import { useTranslation } from "react-i18next";
+import { Workflow, CheckCircle2, XCircle, MinusCircle } from "lucide-react-native";
+import { ScreenHeader } from "~/components/common/ScreenHeader";
+import { Badge } from "~/components/ui/Badge";
+import { EmptyState } from "~/components/ui/EmptyState";
+import { ListSkeleton } from "~/components/ui/ListSkeleton";
+import { runStatusVariant } from "~/components/automation/FlowRunList";
+import { useFlowRun, type FlowStepLog } from "~/hooks/useFlowRuns";
+import { formatDateTime } from "~/lib/formatting";
+
+function humanize(token: string): string {
+ return token.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
+}
+
+function StepIcon({ status }: { status: FlowStepLog["status"] }) {
+ if (status === "success") return ;
+ if (status === "failure") return ;
+ return ;
+}
+
+function StepRow({ step }: { step: FlowStepLog }) {
+ return (
+
+
+
+
+
+
+ {step.nodeLabel ?? step.nodeId}
+
+
+ {humanize(step.nodeType)}
+ {typeof step.durationMs === "number" ? ` · ${step.durationMs} ms` : ""}
+
+ {step.error?.message ? (
+ {step.error.message}
+ ) : null}
+
+
+ );
+}
+
+/**
+ * Flow run detail — shows one execution's status, timing, and per-node step log.
+ */
+export default function FlowRunDetailScreen() {
+ const { name, runId } = useLocalSearchParams<{ name: string; runId: string }>();
+ const flowName = Array.isArray(name) ? name[0] : name;
+ const runIdStr = Array.isArray(runId) ? runId[0] : runId;
+ const { t } = useTranslation();
+ const { data: run, isLoading, error } = useFlowRun(flowName, runIdStr);
+
+ const backFallback = `/flows/${encodeURIComponent(flowName ?? "")}`;
+
+ if (isLoading) {
+ return (
+
+
+
+
+ );
+ }
+
+ if (error || !run) {
+ return (
+
+
+
+
+ );
+ }
+
+ const steps = run.steps ?? [];
+
+ return (
+
+
+
+ {/* Run summary */}
+
+
+ {humanize(run.status)}
+ {run.trigger?.type ? {humanize(run.trigger.type)} : null}
+ {typeof run.durationMs === "number" ? (
+ {`${run.durationMs} ms`}
+ ) : null}
+
+ {run.startedAt ? (
+
+ {formatDateTime(run.startedAt)}
+
+ ) : null}
+ {run.error?.message ? (
+ {run.error.message}
+ ) : null}
+
+
+ {/* Step log */}
+
+
+ {t("workflow.steps")}
+
+ {steps.length === 0 ? (
+ —
+ ) : (
+ steps.map((step, i) => )
+ )}
+
+
+ );
+}
diff --git a/apps/server/objectstack.config.ts b/apps/server/objectstack.config.ts
index 786812a..01144e7 100644
--- a/apps/server/objectstack.config.ts
+++ b/apps/server/objectstack.config.ts
@@ -1,4 +1,5 @@
import { defineStack, type ObjectStackDefinition } from '@objectstack/spec';
+import { AutomationServicePlugin } from '@objectstack/service-automation';
import * as objects from './src/objects';
const stack: ObjectStackDefinition = defineStack({
@@ -12,6 +13,11 @@ const stack: ObjectStackDefinition = defineStack({
},
objects: Object.values(objects),
+
+ // Enable the automation engine so flows can be triggered + leave a run log
+ // (exposes /api/v1/automation/{name}/trigger and /runs). The plugin seeds the
+ // built-in node executors itself (ADR-0018).
+ plugins: [new AutomationServicePlugin()],
});
export default stack;
diff --git a/apps/server/package.json b/apps/server/package.json
index 534457c..d128a7f 100644
--- a/apps/server/package.json
+++ b/apps/server/package.json
@@ -14,7 +14,10 @@
"@objectstack/spec": "^7.5.0",
"@objectstack/runtime": "^7.5.0",
"@objectstack/objectql": "^7.5.0",
- "@objectstack/driver-memory": "^7.5.0"
+ "@objectstack/driver-memory": "^7.5.0",
+ "@objectstack/service-automation": "^7.5.0",
+ "@objectstack/plugin-trigger-record-change": "^7.5.0",
+ "@objectstack/plugin-trigger-schedule": "^7.5.0"
},
"devDependencies": {
"@objectstack/cli": "^7.5.0",
diff --git a/components/automation/FlowRunList.tsx b/components/automation/FlowRunList.tsx
new file mode 100644
index 0000000..35790e5
--- /dev/null
+++ b/components/automation/FlowRunList.tsx
@@ -0,0 +1,69 @@
+import React from "react";
+import { View, Text } from "react-native";
+import { ChevronRight } from "lucide-react-native";
+import { PressableCard } from "~/components/ui/PressableCard";
+import { Badge } from "~/components/ui/Badge";
+import { formatDateTime } from "~/lib/formatting";
+import type { FlowRun } from "~/hooks/useFlowRuns";
+
+type BadgeVariant = "default" | "secondary" | "destructive" | "outline";
+
+/** Map an execution status to a badge variant. */
+export function runStatusVariant(status: string | undefined): BadgeVariant {
+ switch ((status ?? "").toLowerCase()) {
+ case "success":
+ case "completed":
+ return "default";
+ case "failure":
+ case "failed":
+ case "error":
+ return "destructive";
+ case "running":
+ case "pending":
+ case "in_progress":
+ return "secondary";
+ default:
+ return "outline";
+ }
+}
+
+function humanize(token: string): string {
+ return token.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
+}
+
+export interface FlowRunListProps {
+ runs: FlowRun[];
+ onPressRun?: (run: FlowRun) => void;
+}
+
+/** A tappable list of flow executions (status, trigger, time). */
+export function FlowRunList({ runs, onPressRun }: FlowRunListProps) {
+ return (
+
+ {runs.map((run) => (
+ onPressRun?.(run)}
+ className="flex-row items-center rounded-xl border border-border bg-card px-4 py-3"
+ accessibilityLabel={`Run ${run.id}, status ${run.status}`}
+ >
+
+
+ {humanize(run.status)}
+ {run.trigger?.type ? (
+ {humanize(run.trigger.type)}
+ ) : null}
+
+ {run.startedAt ? (
+
+ {formatDateTime(run.startedAt)}
+ {typeof run.durationMs === "number" ? ` · ${run.durationMs} ms` : ""}
+
+ ) : null}
+
+
+
+ ))}
+
+ );
+}
diff --git a/hooks/useFlowRuns.ts b/hooks/useFlowRuns.ts
new file mode 100644
index 0000000..3e713c3
--- /dev/null
+++ b/hooks/useFlowRuns.ts
@@ -0,0 +1,124 @@
+import { useQuery, useQueryClient, type UseQueryResult } from "@tanstack/react-query";
+import { useClient } from "@objectstack/client-react";
+import { useCallback, useState } from "react";
+
+/* ------------------------------------------------------------------ */
+/* Types */
+/* ------------------------------------------------------------------ */
+
+export interface FlowStepLog {
+ nodeId: string;
+ nodeType: string;
+ nodeLabel?: string;
+ status: "success" | "failure" | "skipped";
+ startedAt?: string;
+ completedAt?: string;
+ durationMs?: number;
+ error?: { code?: string; message?: string };
+}
+
+/** A single flow execution, mirroring service-automation `ExecutionLogEntry`. */
+export interface FlowRun {
+ id: string;
+ flowName: string;
+ flowVersion?: number;
+ status: string;
+ startedAt?: string;
+ completedAt?: string;
+ durationMs?: number;
+ trigger?: { type?: string; userId?: string; object?: string; recordId?: string };
+ steps?: FlowStepLog[];
+ error?: { code?: string; message?: string };
+}
+
+export interface FlowRunsResult {
+ runs: FlowRun[];
+ hasMore: boolean;
+}
+
+/* ------------------------------------------------------------------ */
+/* Run history */
+/* ------------------------------------------------------------------ */
+
+/**
+ * List recent executions of a flow (`GET /automation/{name}/runs`). Returns an
+ * empty list (not an error) when the server has no automation engine, so the UI
+ * can render an empty state rather than a failure.
+ */
+export function useFlowRuns(
+ flowName: string | undefined,
+): UseQueryResult {
+ const client = useClient();
+ return useQuery({
+ queryKey: ["flow-runs", flowName],
+ enabled: !!flowName,
+ queryFn: async (): Promise => {
+ const res = await client.automation.listRuns<{
+ runs?: FlowRun[];
+ hasMore?: boolean;
+ }>(flowName!, { limit: 25 });
+ return { runs: res?.runs ?? [], hasMore: res?.hasMore ?? false };
+ },
+ });
+}
+
+/** Fetch a single run with its step log (`GET /automation/{name}/runs/{id}`). */
+export function useFlowRun(
+ flowName: string | undefined,
+ runId: string | undefined,
+): UseQueryResult {
+ const client = useClient();
+ return useQuery({
+ queryKey: ["flow-run", flowName, runId],
+ enabled: !!flowName && !!runId,
+ queryFn: async () => (await client.automation.getRun(flowName!, runId!)) ?? null,
+ });
+}
+
+/* ------------------------------------------------------------------ */
+/* Trigger */
+/* ------------------------------------------------------------------ */
+
+export interface TriggerResult {
+ ok: boolean;
+ error?: string;
+ data?: unknown;
+}
+
+/**
+ * Run a flow on demand (`POST /automation/{name}/trigger`). The engine wraps the
+ * handler result, so an HTTP 200 can still carry `{ success: false, error }` —
+ * surface that as a failure rather than reporting a false success.
+ */
+export function useTriggerFlow(): {
+ trigger: (flowName: string, ctx?: Record) => Promise;
+ isTriggering: boolean;
+} {
+ const client = useClient();
+ const queryClient = useQueryClient();
+ const [isTriggering, setIsTriggering] = useState(false);
+
+ const trigger = useCallback(
+ async (flowName: string, ctx?: Record): Promise => {
+ setIsTriggering(true);
+ try {
+ const data = await client.automation.execute(flowName, ctx ?? {});
+ // Unwrapped handler result may itself signal a logical failure.
+ const inner = data as { success?: boolean; error?: string } | null;
+ if (inner && inner.success === false) {
+ return { ok: false, error: inner.error ?? "Flow execution failed", data };
+ }
+ return { ok: true, data };
+ } catch (err) {
+ return { ok: false, error: err instanceof Error ? err.message : "Flow execution failed" };
+ } finally {
+ setIsTriggering(false);
+ // Refresh the run history regardless of outcome.
+ void queryClient.invalidateQueries({ queryKey: ["flow-runs", flowName] });
+ }
+ },
+ [client, queryClient],
+ );
+
+ return { trigger, isTriggering };
+}
diff --git a/locales/ar.json b/locales/ar.json
index ce3917f..4a0fd8d 100644
--- a/locales/ar.json
+++ b/locales/ar.json
@@ -73,7 +73,15 @@
"updateStatus": "تحديث الحالة",
"moveToConfirm": "نقل هذا السجل إلى \"{{state}}\"؟",
"statusUpdated": "تم تحديث الحالة.",
- "statusUpdateFailed": "فشل تحديث الحالة."
+ "statusUpdateFailed": "فشل تحديث الحالة.",
+ "runFlow": "تشغيل التدفق",
+ "runConfirm": "تشغيل \"{{flow}}\" الآن؟",
+ "runStarted": "بدأ تشغيل التدفق.",
+ "runFailed": "فشل تشغيل التدفق.",
+ "runHistory": "سجل التشغيل",
+ "noRuns": "لا توجد عمليات تشغيل بعد.",
+ "steps": "الخطوات",
+ "runLabel": "تشغيل"
},
"actions": {
"title": "إجراءات",
diff --git a/locales/en.json b/locales/en.json
index 2cbb247..f2cfbda 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -69,7 +69,15 @@
"updateStatus": "Update Status",
"moveToConfirm": "Move this record to \"{{state}}\"?",
"statusUpdated": "Status updated.",
- "statusUpdateFailed": "Failed to update status."
+ "statusUpdateFailed": "Failed to update status.",
+ "runFlow": "Run Flow",
+ "runConfirm": "Run \"{{flow}}\" now?",
+ "runStarted": "Flow run started.",
+ "runFailed": "Flow run failed.",
+ "runHistory": "Run History",
+ "noRuns": "No runs yet.",
+ "steps": "Steps",
+ "runLabel": "Run"
},
"actions": {
"title": "Actions",
diff --git a/locales/zh.json b/locales/zh.json
index e3024ff..75eada4 100644
--- a/locales/zh.json
+++ b/locales/zh.json
@@ -68,7 +68,15 @@
"updateStatus": "更新状态",
"moveToConfirm": "将此记录移动到“{{state}}”?",
"statusUpdated": "状态已更新。",
- "statusUpdateFailed": "更新状态失败。"
+ "statusUpdateFailed": "更新状态失败。",
+ "runFlow": "运行流程",
+ "runConfirm": "立即运行“{{flow}}”?",
+ "runStarted": "流程已开始运行。",
+ "runFailed": "流程运行失败。",
+ "runHistory": "运行历史",
+ "noRuns": "暂无运行记录。",
+ "steps": "步骤",
+ "runLabel": "运行"
},
"actions": {
"title": "操作",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b07e0f0..5d4cb10 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -239,7 +239,7 @@ importers:
version: 19.1.0(react@19.1.0)
ts-jest:
specifier: ^29.4.6
- version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(esbuild@0.28.0)(jest-util@29.7.0)(jest@29.7.0(@types/node@25.2.2))(typescript@5.9.3)
+ version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@25.2.2))(typescript@5.9.3)
tsx:
specifier: ^4.21.0
version: 4.21.0
@@ -307,9 +307,18 @@ importers:
'@objectstack/objectql':
specifier: ^7.5.0
version: 7.5.0(ai@6.0.193(zod@4.4.3))
+ '@objectstack/plugin-trigger-record-change':
+ specifier: ^7.5.0
+ version: 7.5.0(ai@6.0.193(zod@4.4.3))
+ '@objectstack/plugin-trigger-schedule':
+ specifier: ^7.5.0
+ version: 7.5.0(ai@6.0.193(zod@4.4.3))
'@objectstack/runtime':
specifier: ^7.5.0
version: 7.5.0(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(ai@6.0.193(zod@4.4.3))(better-call@1.3.5(zod@4.3.6))(better-sqlite3@12.10.0)(jose@6.1.3)(kysely@0.29.2)(mongodb@7.2.0)(nanostores@1.3.0)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@objectstack/service-automation':
+ specifier: ^7.5.0
+ version: 7.5.0(ai@6.0.193(zod@4.4.3))
'@objectstack/spec':
specifier: ^7.5.0
version: 7.5.0(ai@6.0.193(zod@4.4.3))
@@ -10503,7 +10512,7 @@ snapshots:
'@objectstack/plugin-webhooks': 7.5.0(ai@6.0.193(zod@4.3.6))
'@objectstack/rest': 7.5.0(ai@6.0.193(zod@4.3.6))
'@objectstack/runtime': 7.5.0(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(ai@6.0.193(zod@4.3.6))(better-call@1.3.5(zod@4.3.6))(better-sqlite3@12.10.0)(jose@6.1.3)(kysely@0.29.2)(mongodb@7.2.0)(nanostores@1.3.0)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
- '@objectstack/service-ai': 7.5.0(@ai-sdk/gateway@3.0.121(zod@4.3.6))
+ '@objectstack/service-ai': 7.5.0(@ai-sdk/gateway@3.0.121(zod@4.4.3))
'@objectstack/service-analytics': 7.5.0(ai@6.0.193(zod@4.3.6))
'@objectstack/service-automation': 7.5.0(ai@6.0.193(zod@4.3.6))
'@objectstack/service-cache': 7.5.0(ai@6.0.193(zod@4.3.6))
@@ -10607,7 +10616,7 @@ snapshots:
'@objectstack/plugin-webhooks': 7.5.0(ai@6.0.193(zod@4.4.3))
'@objectstack/rest': 7.5.0(ai@6.0.193(zod@4.4.3))
'@objectstack/runtime': 7.5.0(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(ai@6.0.193(zod@4.4.3))(better-call@1.3.5(zod@4.3.6))(better-sqlite3@12.10.0)(jose@6.1.3)(kysely@0.29.2)(mongodb@7.2.0)(nanostores@1.3.0)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
- '@objectstack/service-ai': 7.5.0(@ai-sdk/gateway@3.0.121(zod@4.3.6))
+ '@objectstack/service-ai': 7.5.0(@ai-sdk/gateway@3.0.121(zod@4.4.3))
'@objectstack/service-analytics': 7.5.0(ai@6.0.193(zod@4.4.3))
'@objectstack/service-automation': 7.5.0(ai@6.0.193(zod@4.4.3))
'@objectstack/service-cache': 7.5.0(ai@6.0.193(zod@4.4.3))
@@ -11386,7 +11395,7 @@ snapshots:
- vitest
- vue
- '@objectstack/service-ai@7.5.0(@ai-sdk/gateway@3.0.121(zod@4.3.6))':
+ '@objectstack/service-ai@7.5.0(@ai-sdk/gateway@3.0.121(zod@4.4.3))':
dependencies:
'@ai-sdk/provider': 3.0.10
'@objectstack/core': 7.5.0(ai@6.0.193(zod@4.4.3))
@@ -18628,7 +18637,7 @@ snapshots:
ts-interface-checker@0.1.13: {}
- ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(esbuild@0.28.0)(jest-util@29.7.0)(jest@29.7.0(@types/node@25.2.2))(typescript@5.9.3):
+ ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@25.2.2))(typescript@5.9.3):
dependencies:
bs-logger: 0.2.6
fast-json-stable-stringify: 2.1.0
@@ -18646,7 +18655,6 @@ snapshots:
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
babel-jest: 29.7.0(@babel/core@7.29.0)
- esbuild: 0.28.0
jest-util: 29.7.0
ts-morph@28.0.0: