@softr/sdk 0.0.87-alpha.4 → 0.0.87-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,10 +21,10 @@ declare const listAgents: _tanstack_react_start7.OptionalFetcher<undefined, unde
21
21
  avatar?: string | null | undefined;
22
22
  thumbnail?: string | null | undefined;
23
23
  }[]>>;
24
- /** One past chat with an agent. */
24
+ /** One past chat with an agent. `title` is null until the agent names the chat. */
25
25
  type AgentChatSummary = {
26
26
  id: string;
27
- title: string;
27
+ title: string | null;
28
28
  };
29
29
  //#endregion
30
30
  export { Agent, AgentChatSummary, listAgents };
@@ -47,6 +47,7 @@ const chatSchema = z.object({
47
47
  sessionId: z.string().nullable(),
48
48
  timezone: z.string(),
49
49
  pageName: z.string(),
50
+ pageUrl: z.string(),
50
51
  record: z.object({
51
52
  id: z.string(),
52
53
  fields: z.record(z.string(), z.unknown())
@@ -75,6 +76,7 @@ const agentChatFn = createServerFn({ method: "POST" }).validator((data) => {
75
76
  ...data.sessionId ? { sessionId: data.sessionId } : {},
76
77
  context: {
77
78
  pageName: data.pageName,
79
+ pageUrl: data.pageUrl,
78
80
  ...data.record ? { currentRecord: { data: data.record } } : {}
79
81
  }
80
82
  }
@@ -105,7 +107,7 @@ const listAgentChatsFn = createServerFn({ method: "POST" }).validator(z.object({
105
107
  if (!res.ok) throw new Error(`Loading chats failed: ${await describeFailure(res)}`);
106
108
  return z.array(z.object({
107
109
  id: z.string(),
108
- title: z.string()
110
+ title: z.string().nullable()
109
111
  })).parse(await res.json());
110
112
  });
111
113
  const historySchema = z.object({
@@ -128,21 +130,23 @@ const historySchema = z.object({
128
130
  })
129
131
  }))
130
132
  });
133
+ const CHAT_GONE = [
134
+ 401,
135
+ 403,
136
+ 404,
137
+ 410
138
+ ];
131
139
  /** What a past chat holds, or `null` when it's gone or belongs to someone else. */
132
140
  const loadAgentChatFn = createServerFn({ method: "POST" }).validator(z.object({
133
141
  agentId: z.string().min(1),
134
142
  sessionId: z.string().min(1)
135
143
  })).handler(async ({ data }) => {
136
144
  const res = await agentsFetch(`${encodeURIComponent(data.agentId)}/sessions/${encodeURIComponent(data.sessionId)}`);
137
- if ([
138
- 401,
139
- 403,
140
- 404,
141
- 410
142
- ].includes(res.status)) return null;
145
+ if (CHAT_GONE.includes(res.status)) return null;
143
146
  if (!res.ok) throw new Error(`Loading the chat failed: ${await describeFailure(res)}`);
144
147
  return historySchema.parse(await res.json());
145
148
  });
149
+ /** Saves the visitor's answer. `false` when the chat is gone, see `loadAgentChatFn`. */
146
150
  const decideAgentToolFn = createServerFn({ method: "POST" }).validator(z.object({
147
151
  agentId: z.string().min(1),
148
152
  sessionId: z.string().min(1),
@@ -159,7 +163,9 @@ const decideAgentToolFn = createServerFn({ method: "POST" }).validator(z.object(
159
163
  method: "POST",
160
164
  body: decision
161
165
  });
166
+ if (CHAT_GONE.includes(res.status)) return false;
162
167
  if (!res.ok) throw new Error(`Saving the answer failed: ${await describeFailure(res)}`);
168
+ return true;
163
169
  });
164
170
 
165
171
  //#endregion
@@ -15,6 +15,10 @@ type AgentToolCall = {
15
15
  status: "running" | "done" | "failed" | "awaiting-approval" | "approved" | "rejected" | "awaiting-connection";
16
16
  error?: string;
17
17
  integration?: string;
18
+ /** Why the agent calls it, in its own words. */
19
+ thought?: string;
20
+ /** How long a finished call took. */
21
+ durationMs?: number;
18
22
  };
19
23
  type AgentTodo = {
20
24
  content: string;
@@ -55,6 +59,8 @@ type AgentChat = {
55
59
  messages: AgentMessage[];
56
60
  /** `thinking` until the first part of the reply arrives, `replying` until it ends. */
57
61
  status: "idle" | "thinking" | "replying";
62
+ /** True while a past chat loads, including the one reopened on mount. */
63
+ loading: boolean;
58
64
  /** Why the last request failed. Cleared by the next one. */
59
65
  error: Error | null;
60
66
  /** The chat the next message goes to, `null` for a new one. */
@@ -1,4 +1,5 @@
1
1
  import { assert } from "../assertions.js";
2
+ import { useCurrentUser } from "../auth/isomorphic.js";
2
3
  import { agentChatFn, decideAgentToolFn, listAgentChatsFn, loadAgentChatFn } from "./agent-fns.js";
3
4
  import { useCallback, useEffect, useRef, useState } from "react";
4
5
 
@@ -15,8 +16,10 @@ function useAgentChat(agentId, options = {}) {
15
16
  const [messages, setMessages] = useState([]);
16
17
  const [live, setLive] = useState(null);
17
18
  const [status, setStatus] = useState("idle");
19
+ const [loading, setLoading] = useState(false);
18
20
  const [error, setError] = useState(null);
19
21
  const [chatId, setChatId] = useState(null);
22
+ const storeKey = chatKey(agentId, useCurrentUser()?.id ?? null);
20
23
  const chatIdRef = useRef(chatId);
21
24
  chatIdRef.current = chatId;
22
25
  const recordRef = useRef(options.record ?? null);
@@ -48,6 +51,7 @@ function useAgentChat(agentId, options = {}) {
48
51
  sessionId: chatIdRef.current,
49
52
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
50
53
  pageName: document.title,
54
+ pageUrl: location.href,
51
55
  record: recordRef.current
52
56
  };
53
57
  const form = new FormData();
@@ -67,6 +71,7 @@ function useAgentChat(agentId, options = {}) {
67
71
  if (event.sessionId && event.sessionId !== chatIdRef.current) {
68
72
  chatIdRef.current = event.sessionId;
69
73
  setChatId(event.sessionId);
74
+ saveChatId(storeKey, event.sessionId);
70
75
  }
71
76
  blocks = reduceEvent(blocks, event);
72
77
  setLive(blocks);
@@ -89,7 +94,11 @@ function useAgentChat(agentId, options = {}) {
89
94
  abortRef.current = null;
90
95
  commitLive(blocks);
91
96
  setStatus("idle");
92
- }, [agentId, commitLive]);
97
+ }, [
98
+ agentId,
99
+ storeKey,
100
+ commitLive
101
+ ]);
93
102
  const send = useCallback((text) => {
94
103
  const trimmed = text.trim();
95
104
  if (!trimmed) return;
@@ -117,14 +126,17 @@ function useAgentChat(agentId, options = {}) {
117
126
  abortRef.current = null;
118
127
  chatIdRef.current = id;
119
128
  setChatId(id);
129
+ saveChatId(storeKey, id);
120
130
  setMessages([]);
121
131
  setLive(null);
122
132
  setStatus("idle");
133
+ setLoading(false);
123
134
  setError(null);
124
- }, []);
135
+ }, [storeKey]);
125
136
  const newChat = useCallback(() => reset(null), [reset]);
126
- const openChat = useCallback((id) => {
137
+ const load = useCallback((id, restoring) => {
127
138
  reset(id);
139
+ setLoading(true);
128
140
  loadAgentChatFn({ data: {
129
141
  agentId,
130
142
  sessionId: id
@@ -132,14 +144,27 @@ function useAgentChat(agentId, options = {}) {
132
144
  if (chatIdRef.current !== id) return;
133
145
  if (history === null) {
134
146
  reset(null);
135
- setError(/* @__PURE__ */ new Error("This chat is no longer available. Start a new one."));
147
+ if (!restoring) setError(new Error(CHAT_GONE_MESSAGE));
136
148
  return;
137
149
  }
150
+ setLoading(false);
138
151
  setMessages(fromHistory(history));
139
152
  }).catch((err) => {
140
- if (chatIdRef.current === id) setError(err instanceof Error ? err : new Error(String(err)));
153
+ if (chatIdRef.current !== id) return;
154
+ setLoading(false);
155
+ setError(err instanceof Error ? err : new Error(String(err)));
141
156
  });
142
157
  }, [agentId, reset]);
158
+ const openChat = useCallback((id) => load(id, false), [load]);
159
+ useEffect(() => {
160
+ const saved = savedChatId(storeKey);
161
+ if (saved) load(saved, true);
162
+ else newChat();
163
+ }, [
164
+ storeKey,
165
+ load,
166
+ newChat
167
+ ]);
143
168
  const listChats = useCallback(() => listAgentChatsFn({ data: { agentId } }), [agentId]);
144
169
  const setToolStatus = useCallback((toolCallId, next) => {
145
170
  const update = (blocks) => blocks.map((b) => b.type === "tool" && b.id === toolCallId ? {
@@ -164,7 +189,12 @@ function useAgentChat(agentId, options = {}) {
164
189
  toolName: tool.name,
165
190
  decision,
166
191
  scope
167
- } }).then(() => {
192
+ } }).then((saved) => {
193
+ if (!saved) {
194
+ reset(null);
195
+ setError(new Error(CHAT_GONE_MESSAGE));
196
+ return;
197
+ }
168
198
  if (decision !== "APPROVE") return;
169
199
  abortRef.current?.abort();
170
200
  commitLive(liveRef.current);
@@ -178,7 +208,8 @@ function useAgentChat(agentId, options = {}) {
178
208
  messages,
179
209
  setToolStatus,
180
210
  commitLive,
181
- stream
211
+ stream,
212
+ reset
182
213
  ]);
183
214
  const approve = useCallback((toolCallId, scope = "ONCE") => decide(toolCallId, "APPROVE", scope), [decide]);
184
215
  const reject = useCallback((toolCallId) => decide(toolCallId, "REJECT", "ONCE"), [decide]);
@@ -189,6 +220,7 @@ function useAgentChat(agentId, options = {}) {
189
220
  blocks: live
190
221
  }],
191
222
  status,
223
+ loading,
192
224
  error,
193
225
  chatId,
194
226
  send,
@@ -231,20 +263,23 @@ function reduceEvent(blocks, event) {
231
263
  if (event.toolName === QUESTION_TOOL) return blocks;
232
264
  if (blocks.some((b) => b.type === "tool" && b.id === event.toolCallId)) return updateTool(blocks, event.toolCallId, (b) => b.status === "running" ? {
233
265
  ...b,
234
- input: event.input ?? b.input
266
+ input: event.input ?? b.input,
267
+ thought: event.innerThought ?? b.thought
235
268
  } : b);
236
269
  return [...blocks, {
237
270
  type: "tool",
238
271
  id: event.toolCallId,
239
272
  name: event.toolName,
240
273
  input: event.input,
274
+ thought: event.innerThought,
241
275
  integration: event.integrationType,
242
276
  status: "running"
243
277
  }];
244
278
  case "TOOL_CALL_END": return updateTool(blocks, event.toolCallId, (b) => b.status === "running" || b.status === "approved" ? {
245
279
  ...b,
246
280
  status: event.failed ? "failed" : "done",
247
- error: event.errorMessage
281
+ error: event.errorMessage,
282
+ durationMs: event.durationMs
248
283
  } : b);
249
284
  case "TOOL_PERMISSION_REQUEST":
250
285
  case "INTEGRATION_AUTH_REQUEST": {
@@ -343,6 +378,8 @@ function fromHistory(history) {
343
378
  id: call.id,
344
379
  name: call.toolName,
345
380
  input: call.input,
381
+ thought: call.metadata.innerThought ?? void 0,
382
+ durationMs: call.metadata.durationMs ?? void 0,
346
383
  status: call.metadata.failed ? "failed" : "done"
347
384
  });
348
385
  }
@@ -357,6 +394,21 @@ function fromHistory(history) {
357
394
  };
358
395
  });
359
396
  }
397
+ const CHAT_GONE_MESSAGE = "This chat is no longer available. Start a new one.";
398
+ const chatKey = (agentId, userId) => `softr-agent-chat:${agentId}:${userId ?? "visitor"}`;
399
+ function savedChatId(key) {
400
+ try {
401
+ return localStorage.getItem(key);
402
+ } catch {
403
+ return null;
404
+ }
405
+ }
406
+ function saveChatId(key, chatId) {
407
+ try {
408
+ if (chatId) localStorage.setItem(key, chatId);
409
+ else localStorage.removeItem(key);
410
+ } catch {}
411
+ }
360
412
  async function failureMessage(res) {
361
413
  const body = await res.json().catch(() => null);
362
414
  if (typeof body === "object" && body !== null && "message" in body && typeof body.message === "string") return body.message;
@@ -1,4 +1,4 @@
1
- import * as react6 from "react";
1
+ import * as react4 from "react";
2
2
 
3
3
  //#region src/default-error-overlay.d.ts
4
4
  type Props = {
@@ -8,6 +8,6 @@ type Props = {
8
8
  declare function DefaultErrorOverlay({
9
9
  error,
10
10
  reset
11
- }: Props): react6.JSX.Element;
11
+ }: Props): react4.JSX.Element;
12
12
  //#endregion
13
13
  export { DefaultErrorOverlay };
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.0.87-alpha.4";
2
+ var version = "0.0.87-alpha.5";
3
3
 
4
4
  //#endregion
5
5
  export { version };
package/dist/pwa.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import * as react4 from "react";
1
+ import * as react5 from "react";
2
2
  import "@tanstack/react-start";
3
3
 
4
4
  //#region src/pwa.d.ts
@@ -23,7 +23,7 @@ import "@tanstack/react-start";
23
23
  * )
24
24
  * }
25
25
  */
26
- declare function SoftrPwaHead(): react4.FunctionComponentElement<react4.FragmentProps>;
26
+ declare function SoftrPwaHead(): react5.FunctionComponentElement<react5.FragmentProps>;
27
27
  type PwaPlatform = "ios" | "android" | "desktop";
28
28
  type PwaInstall = {
29
29
  available: false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softr/sdk",
3
- "version": "0.0.87-alpha.4",
3
+ "version": "0.0.87-alpha.5",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
@@ -8,7 +8,7 @@ description: >-
8
8
  metadata:
9
9
  type: core
10
10
  library: softr-sdk
11
- library_version: 0.0.87-alpha.4
11
+ library_version: 0.0.87-alpha.5
12
12
  ---
13
13
 
14
14
  # Softr SDK core
@@ -10,7 +10,7 @@ description: >-
10
10
  metadata:
11
11
  type: sub-skill
12
12
  library: softr-sdk
13
- library_version: 0.0.87-alpha.4
13
+ library_version: 0.0.87-alpha.5
14
14
  ---
15
15
 
16
16
  **Boundary.** `listAgents` and `useAgentChat` live on `@softr/sdk`. `listAgents` runs in a route loader, `useAgentChat` in a component. Neither needs a route, server function or registration of its own. It chats as the signed-in visitor, or anonymously when no one is signed in.
@@ -58,9 +58,10 @@ function AgentPanel({ agentId, ticket }: { agentId: string; ticket: Ticket | nul
58
58
 
59
59
  - `messages`: the whole chat, oldest first. While the agent answers, its reply is the last message and grows as it streams.
60
60
  - `status`: `thinking` until the first part of the reply arrives, `replying` until it ends, then `idle`. Show a typing hint while it's `thinking`.
61
+ - `loading`: `true` while a past chat loads. Show a spinner, not the empty-chat greeting.
61
62
  - `error`: why the last request failed, or `null`.
62
63
  - `send(text)`, `stop()`, `newChat()`.
63
- - `listChats()` resolves to the visitor's past chats, `{ id, title }[]`. `openChat(id)` loads one. `chatId` is the open one, `null` for a new chat.
64
+ - `listChats()` resolves to the visitor's past chats, `{ id, title }[]`, with `title` `null` until the agent names the chat. `openChat(id)` loads one. `chatId` is the open one, `null` for a new chat. The open chat is kept in the browser for each visitor, so the hook reopens it after a remount or a reload.
64
65
  - `approve(toolCallId, scope?)` and `reject(toolCallId)` answer a tool waiting for approval. `scope` is `ONCE` (default), `SESSION` for the rest of this chat, or `USER` for always.
65
66
 
66
67
  ## Rendering a message
@@ -70,7 +71,7 @@ Each message has `role` (`user` or `assistant`) and `blocks`, in the order the a
70
71
  | Block | Show it as |
71
72
  | --- | --- |
72
73
  | `text` | Markdown text |
73
- | `tool` | One line: the tool `name` and its `status`. Collapse finished ones. |
74
+ | `tool` | One line: the tool `name` and its `status`. Collapse finished ones. Expanded, show `thought`, `input`, `error` and `durationMs`. |
74
75
  | `todo` | A checklist of the agent's plan. It updates in place. |
75
76
  | `question` | The questions with their options. A click on an option sends its label with `send`. |
76
77
  | `error` | An error line inside the reply |
@@ -79,11 +80,25 @@ A `tool` with status `awaiting-approval` blocks the agent until the visitor answ
79
80
 
80
81
  A `tool` with status `awaiting-connection` needs the visitor to connect the `integration` first. Code apps can't do that yet. Say which integration the agent needs, and that the builder has to connect it for this agent in Studio.
81
82
 
83
+ ## The chat panel
84
+
85
+ Classic Softr apps ship a chat panel. Visitors expect the same from yours:
86
+
87
+ - A round button in a corner opens it. A right-side panel on desktop, full screen on mobile.
88
+ - The header shows the agent's avatar and name, a picker when there are several agents, new chat, past chats and close.
89
+ - An empty chat greets the visitor with the agent's avatar.
90
+ - The list scrolls to the newest message as the reply streams, until the visitor scrolls up. Then a button takes them back down.
91
+ - Text renders as Markdown, tables included.
92
+ - The input says "Ask {agent title}…". Enter sends. Shift+Enter adds a line.
93
+ - A line under the input: "AI can make mistakes. Please double-check responses."
94
+ - A question with one choice sends on click. One with `multiSelect` or several questions sends once all are answered. An answered question stays locked.
95
+ - Approve offers the three scopes: once, for this chat, always for this agent.
96
+
82
97
  ## Context
83
98
 
84
99
  `record` is the one record the visitor is looking at, so they can say "close this one". Pass it where a page shows one record, and `null` elsewhere. Use readable field names as keys. The agent reads them as they are.
85
100
 
86
- The page title goes along on its own. Nothing else about the page does, so don't try to send whole lists.
101
+ The page title and URL go along on their own. Nothing else about the page does, so don't try to send whole lists.
87
102
 
88
103
  ## Letting the agent act on the app
89
104
 
@@ -95,6 +110,8 @@ The agent reads and changes data through the app's own MCP, as the visitor who i
95
110
 
96
111
  **Rendering only `text` blocks.** A tool waiting for approval never shows, and the agent waits forever. Render every block type.
97
112
 
113
+ **The hook inside a panel that unmounts when it closes.** Closing it stops a reply that is still coming. Call `useAgentChat` in the component that owns the open state, such as the layout, and pass `chat` into the panel. Keep the selected agent there too, or closing the panel resets it.
114
+
98
115
  **An agent id written by hand.** The chat fails on the first message, and an agent hidden from the visitor still shows. Take the id from `listAgents`.
99
116
 
100
117
  **Expecting the agent to change data with no MCP wired.** It answers but can't act. List the actions it needs in `softrMcpMiddleware`.
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.87-alpha.4
12
+ library_version: 0.0.87-alpha.5
13
13
  ---
14
14
 
15
15
  **Boundary.** `auth` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build.
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.87-alpha.4
12
+ library_version: 0.0.87-alpha.5
13
13
  ---
14
14
 
15
15
  **Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler or a server-side route hook — importing it into a component fails the build. `defineQuery` and every type come from `@softr/sdk` and can be imported anywhere.
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.87-alpha.4
12
+ library_version: 0.0.87-alpha.5
13
13
  ---
14
14
 
15
15
  # The current user
@@ -7,7 +7,7 @@ description: >-
7
7
  metadata:
8
8
  type: sub-skill
9
9
  library: softr-sdk
10
- library_version: 0.0.87-alpha.4
10
+ library_version: 0.0.87-alpha.5
11
11
  ---
12
12
 
13
13
  **Boundary.** `upload` lives on `@softr/sdk/server` and runs inside server functions. `useFileUpload` and `FileItem` live on `@softr/sdk` and run in components. `FileAttachment` comes from `@softr/sdk` and can be imported anywhere.
@@ -8,7 +8,7 @@ description: >-
8
8
  metadata:
9
9
  type: sub-skill
10
10
  library: softr-sdk
11
- library_version: 0.0.87-alpha.4
11
+ library_version: 0.0.87-alpha.5
12
12
  ---
13
13
 
14
14
  # Dates, times and numbers
@@ -7,7 +7,7 @@ description: >-
7
7
  metadata:
8
8
  type: sub-skill
9
9
  library: softr-sdk
10
- library_version: 0.0.87-alpha.4
10
+ library_version: 0.0.87-alpha.5
11
11
  ---
12
12
 
13
13
  **Boundary.** `integration` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineIntegration` and every type come from `@softr/sdk` and can be imported anywhere.
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.87-alpha.4
12
+ library_version: 0.0.87-alpha.5
13
13
  ---
14
14
 
15
15
  **Boundary.** `softrMcpMiddleware` lives on `@softr/sdk` and is registered once, in `src/start.ts`. The actions you hand it are the same `defineQuery` / `defineMutation` values the app's own pages use — declared wherever they already live, not duplicated for MCP.
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.87-alpha.4
12
+ library_version: 0.0.87-alpha.5
13
13
  ---
14
14
 
15
15
  # Installing the app
@@ -8,7 +8,7 @@ description: >-
8
8
  metadata:
9
9
  type: sub-skill
10
10
  library: softr-sdk
11
- library_version: 0.0.87-alpha.4
11
+ library_version: 0.0.87-alpha.5
12
12
  ---
13
13
 
14
14
  **Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineMutation` and every type come from `@softr/sdk` and can be imported anywhere.
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.87-alpha.4
12
+ library_version: 0.0.87-alpha.5
13
13
  ---
14
14
 
15
15
  **Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler or a server-side route hook — importing it into a component fails the build. The same holds for any helper that calls `ds`: declare it inside the handler. A function at module level that calls `ds` keeps the server-only import in the browser bundle, and every page stops loading until it moves. `defineQuery` and every type come from `@softr/sdk` and can be imported anywhere.
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.87-alpha.4
12
+ library_version: 0.0.87-alpha.5
13
13
  ---
14
14
 
15
15
  **Boundary.** `Audience` and `defineVisibility` come from `@softr/sdk` and work anywhere.
@@ -8,7 +8,7 @@ description: >-
8
8
  metadata:
9
9
  type: sub-skill
10
10
  library: softr-sdk
11
- library_version: 0.0.87-alpha.4
11
+ library_version: 0.0.87-alpha.5
12
12
  ---
13
13
 
14
14
  **Boundary.** `defineVisibility`, the access functions and the error predicates come from `@softr/sdk` and work anywhere.
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.87-alpha.4
12
+ library_version: 0.0.87-alpha.5
13
13
  ---
14
14
 
15
15
  **Boundary.** `workflow` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineWorkflow` and every type come from `@softr/sdk` and can be imported anywhere.