@softr/sdk 0.0.93-alpha.1 → 0.0.93-alpha.2

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.
@@ -1,4 +1,5 @@
1
1
  import { EmailAction } from "../action.js";
2
+ import { StepError, StepErrorCode } from "./step.js";
2
3
 
3
4
  //#region src/core/email.d.ts
4
5
  /** A file the email carries, by the URL the app holds it under. */
@@ -32,20 +33,14 @@ interface EmailSent {
32
33
  /** Null when the mail provider did not report one; the email was still sent. */
33
34
  messageId: string | null;
34
35
  }
35
- declare const EMAIL_ERROR_CODES: readonly ["NOT_FOUND", "INVALID_INPUT", "UNAVAILABLE", "TIMEOUT"];
36
- type EmailErrorCode = (typeof EMAIL_ERROR_CODES)[number];
36
+ type EmailErrorCode = StepErrorCode;
37
37
  /**
38
- * Thrown when Softr did not send the email. `code` says why:
39
- *
40
- * - `NOT_FOUND`: no such email action, or the caller is not in its audience.
41
- * - `INVALID_INPUT`: a missing subject or body, an entry that is not exactly one address, more than 50 recipients, an attachment that is not one of the app's uploads, or an idempotency key over 128 characters.
42
- * - `UNAVAILABLE`: Softr could not send it, for example with no workflow credits left, past the Softr fallback
43
- * address's daily limit or with an unconfirmed sender, or could not be reached.
44
- * - `TIMEOUT`: no answer in time. It may still have been sent; retry with the same idempotency key.
38
+ * Thrown when Softr did not send the email; a {@link StepError}, so `code` says why. For an email, `INVALID_INPUT`
39
+ * is a missing subject or body, an entry that is not exactly one address, more than 50 recipients, an attachment
40
+ * that is not one of the app's uploads, or an idempotency key over 128 characters; `UNAVAILABLE` includes the Softr
41
+ * fallback address past its daily limit and an unconfirmed sender.
45
42
  */
46
- declare class EmailError extends Error {
47
- readonly status: number;
48
- readonly code: EmailErrorCode;
43
+ declare class EmailError extends StepError {
49
44
  constructor(status: number, code: EmailErrorCode, message: string);
50
45
  }
51
46
  /** Sends one email through the action. Never retries by itself. */
@@ -3,33 +3,21 @@ import { softrApiUrl } from "../softr-api.js";
3
3
  import { fetch } from "../fetch.js";
4
4
  import { sessionConfig } from "../auth/config.js";
5
5
  import { readSessionToken } from "../auth/session.js";
6
+ import { StepError, readStepFailure } from "./step.js";
6
7
 
7
8
  //#region src/core/email.ts
8
9
  /** Longer than Softr waits for the send itself, so its own timeout answers first. */
9
10
  const TIMEOUT_MS = 12e4;
10
- const EMAIL_ERROR_CODES = [
11
- "NOT_FOUND",
12
- "INVALID_INPUT",
13
- "UNAVAILABLE",
14
- "TIMEOUT"
15
- ];
16
11
  /**
17
- * Thrown when Softr did not send the email. `code` says why:
18
- *
19
- * - `NOT_FOUND`: no such email action, or the caller is not in its audience.
20
- * - `INVALID_INPUT`: a missing subject or body, an entry that is not exactly one address, more than 50 recipients, an attachment that is not one of the app's uploads, or an idempotency key over 128 characters.
21
- * - `UNAVAILABLE`: Softr could not send it, for example with no workflow credits left, past the Softr fallback
22
- * address's daily limit or with an unconfirmed sender, or could not be reached.
23
- * - `TIMEOUT`: no answer in time. It may still have been sent; retry with the same idempotency key.
12
+ * Thrown when Softr did not send the email; a {@link StepError}, so `code` says why. For an email, `INVALID_INPUT`
13
+ * is a missing subject or body, an entry that is not exactly one address, more than 50 recipients, an attachment
14
+ * that is not one of the app's uploads, or an idempotency key over 128 characters; `UNAVAILABLE` includes the Softr
15
+ * fallback address past its daily limit and an unconfirmed sender.
24
16
  */
25
- var EmailError = class extends Error {
26
- status;
27
- code;
17
+ var EmailError = class extends StepError {
28
18
  constructor(status, code, message) {
29
- super(message);
19
+ super(status, code, message);
30
20
  this.name = "EmailError";
31
- this.status = status;
32
- this.code = code;
33
21
  }
34
22
  };
35
23
  /** Sends one email through the action. Never retries by itself. */
@@ -65,10 +53,7 @@ async function sendEmail(action, init) {
65
53
  assert(typeof sent === "object" && sent !== null && "messageId" in sent && (typeof sent.messageId === "string" || sent.messageId === null), "the send answered without a messageId");
66
54
  return { messageId: sent.messageId };
67
55
  }
68
- const failure = await res.json().catch(() => null);
69
- const known = typeof failure === "object" && failure !== null ? failure : {};
70
- const code = ("code" in known ? EMAIL_ERROR_CODES.find((errorCode) => errorCode === known.code) : void 0) ?? (res.status === 404 ? "NOT_FOUND" : res.status === 400 ? "INVALID_INPUT" : "UNAVAILABLE");
71
- const message = "message" in known && typeof known.message === "string" && known.message !== "" ? known.message : `Email "${action.id}" failed with HTTP ${res.status}`;
56
+ const { code, message } = await readStepFailure(res, `Email "${action.id}" failed with HTTP ${res.status}`);
72
57
  throw new EmailError(res.status, code, message);
73
58
  }
74
59
  const email = { send: sendEmail };
@@ -0,0 +1,20 @@
1
+ //#region src/core/step.d.ts
2
+ /** Why Softr did not run a workflow step the app runs by itself, whatever the step. */
3
+ declare const STEP_ERROR_CODES: readonly ["NOT_FOUND", "INVALID_INPUT", "UNAVAILABLE", "TIMEOUT", "RATE_LIMITED"];
4
+ type StepErrorCode = (typeof STEP_ERROR_CODES)[number];
5
+ /**
6
+ * Thrown when Softr did not run a step the app runs by itself, such as sending an email. `code` says why:
7
+ *
8
+ * - `NOT_FOUND`: no such action, or the caller is not in its audience.
9
+ * - `INVALID_INPUT`: the call's inputs were refused; the message says which.
10
+ * - `UNAVAILABLE`: Softr could not run it, for example with no workflow credits left, or could not be reached.
11
+ * - `TIMEOUT`: no answer in time. It may still have run; retry with the same idempotency key.
12
+ * - `RATE_LIMITED`: the app ran its actions too often in the last minute or day; try again later.
13
+ */
14
+ declare class StepError extends Error {
15
+ readonly status: number;
16
+ readonly code: StepErrorCode;
17
+ constructor(status: number, code: StepErrorCode, message: string);
18
+ }
19
+ //#endregion
20
+ export { StepError, StepErrorCode };
@@ -0,0 +1,48 @@
1
+ //#region src/core/step.ts
2
+ /** Why Softr did not run a workflow step the app runs by itself, whatever the step. */
3
+ const STEP_ERROR_CODES = [
4
+ "NOT_FOUND",
5
+ "INVALID_INPUT",
6
+ "UNAVAILABLE",
7
+ "TIMEOUT",
8
+ "RATE_LIMITED"
9
+ ];
10
+ /**
11
+ * Thrown when Softr did not run a step the app runs by itself, such as sending an email. `code` says why:
12
+ *
13
+ * - `NOT_FOUND`: no such action, or the caller is not in its audience.
14
+ * - `INVALID_INPUT`: the call's inputs were refused; the message says which.
15
+ * - `UNAVAILABLE`: Softr could not run it, for example with no workflow credits left, or could not be reached.
16
+ * - `TIMEOUT`: no answer in time. It may still have run; retry with the same idempotency key.
17
+ * - `RATE_LIMITED`: the app ran its actions too often in the last minute or day; try again later.
18
+ */
19
+ var StepError = class extends Error {
20
+ status;
21
+ code;
22
+ constructor(status, code, message) {
23
+ super(message);
24
+ this.name = "StepError";
25
+ this.status = status;
26
+ this.code = code;
27
+ }
28
+ };
29
+ /** The code and message of a step Softr refused, read from its answer; `fallbackMessage` when it sent none. */
30
+ async function readStepFailure(res, fallbackMessage) {
31
+ const failure = await res.json().catch(() => null);
32
+ const known = typeof failure === "object" && failure !== null ? failure : {};
33
+ return {
34
+ code: ("code" in known ? STEP_ERROR_CODES.find((errorCode) => errorCode === known.code) : void 0) ?? codeOfStatus(res.status),
35
+ message: "message" in known && typeof known.message === "string" && known.message !== "" ? known.message : fallbackMessage
36
+ };
37
+ }
38
+ function codeOfStatus(status) {
39
+ switch (status) {
40
+ case 400: return "INVALID_INPUT";
41
+ case 404: return "NOT_FOUND";
42
+ case 429: return "RATE_LIMITED";
43
+ default: return "UNAVAILABLE";
44
+ }
45
+ }
46
+
47
+ //#endregion
48
+ export { StepError, readStepFailure };
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.0.93-alpha.1";
2
+ var version = "0.0.93-alpha.2";
3
3
 
4
4
  //#endregion
5
5
  export { version };
package/dist/server.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { upload } from "./core/file-upload.js";
2
2
  import { AiError, ai } from "./core/ai.js";
3
+ import { StepError, StepErrorCode } from "./core/step.js";
3
4
  import { EmailError, EmailErrorCode, email } from "./core/email.js";
4
5
  import { auth } from "./auth/server.js";
5
6
  import { FieldChoice, RecordOption, RecordOptions } from "./core/options.js";
@@ -7,4 +8,4 @@ import { ds } from "./datasource/datasource.js";
7
8
  import { workflow } from "./core/workflow.js";
8
9
  import { integration } from "./core/integration.js";
9
10
  import "@tanstack/react-start/server-only";
10
- export { AiError, EmailError, type EmailErrorCode, type FieldChoice, type RecordOption, type RecordOptions, ai, auth, ds, email, integration, upload, workflow };
11
+ export { AiError, EmailError, type EmailErrorCode, type FieldChoice, type RecordOption, type RecordOptions, StepError, type StepErrorCode, ai, auth, ds, email, integration, upload, workflow };
package/dist/server.js CHANGED
@@ -4,7 +4,8 @@ import { auth } from "./auth/server.js";
4
4
  import { workflow } from "./core/workflow.js";
5
5
  import { integration } from "./core/integration.js";
6
6
  import { AiError, ai } from "./core/ai.js";
7
+ import { StepError } from "./core/step.js";
7
8
  import { EmailError, email } from "./core/email.js";
8
9
  import "@tanstack/react-start/server-only";
9
10
 
10
- export { AiError, EmailError, ai, auth, ds, email, integration, upload, workflow };
11
+ export { AiError, EmailError, StepError, ai, auth, ds, email, integration, upload, workflow };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softr/sdk",
3
- "version": "0.0.93-alpha.1",
3
+ "version": "0.0.93-alpha.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
@@ -9,7 +9,7 @@ description: >-
9
9
  metadata:
10
10
  type: core
11
11
  library: softr-sdk
12
- library_version: 0.0.93-alpha.1
12
+ library_version: 0.0.93-alpha.2
13
13
  ---
14
14
 
15
15
  # Softr SDK core
@@ -13,7 +13,7 @@ description: >-
13
13
  metadata:
14
14
  type: sub-skill
15
15
  library: softr-sdk
16
- library_version: 0.0.93-alpha.1
16
+ library_version: 0.0.93-alpha.2
17
17
  ---
18
18
 
19
19
  **Boundary.** `ai` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineAiAction` 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.93-alpha.1
12
+ library_version: 0.0.93-alpha.2
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.93-alpha.1
12
+ library_version: 0.0.93-alpha.2
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.93-alpha.1
12
+ library_version: 0.0.93-alpha.2
13
13
  ---
14
14
 
15
15
  # The current user
@@ -12,7 +12,7 @@ description: >-
12
12
  metadata:
13
13
  type: sub-skill
14
14
  library: softr-sdk
15
- library_version: 0.0.93-alpha.1
15
+ library_version: 0.0.93-alpha.2
16
16
  ---
17
17
 
18
18
  **Boundary.** `email` and `EmailError` live on `@softr/sdk/server` and run only inside a `createServerFn` handler — importing them into a component fails the build. `defineEmailAction` and every type come from `@softr/sdk` and can be imported anywhere.
@@ -36,13 +36,14 @@ The action's `permissions` is its audience: everyone in it can send an email thr
36
36
 
37
37
  Every email comes from the app's email sender, unless the action names one of the workspace's senders with `senderId`.
38
38
 
39
- - Read the app's sender with `application_get` before you promise an address. That tool shows the sign-in emails' default, an `@softr.app` address, when no sender is set. Emails from an email action then come from Softr's address instead: the workspace's name at `<workspaceId>@softr-email.com`, limited to 30 emails a day for the whole workspace, its workflows included. That address is for testing only. Say so in your closing message when no sender is set, and that the app needs its own sender before real users rely on its emails.
40
- - Changing the app's sender also changes the address its sign-in emails come from. Ask the user before you call `application_configure_email_sender`, and name both effects.
41
- - Set `senderId` only when the user wants this email from a specific address, such as `billing@`. Pick it from `workspace_list_email_senders`, copied exactly. A sender the mail provider has not confirmed makes every send fail until it is confirmed.
39
+ - Read the app's sender with `application_get` before you say where the emails come from, and name it. Never write "if no sender is set": read it. That tool shows the sign-in emails' default, an `@softr.app` address, when no sender is set. Emails from an email action then come from Softr's address instead: the workspace's name at `<workspaceId>@softr-email.com`, limited to 30 emails a day for the whole workspace, its workflows included. That address is for testing only. Say so in your closing message when no sender is set, and that the app needs its own sender before real users rely on its emails.
40
+ - When the app has no sender of its own, list the workspace's senders with `workspace_list_email_senders` and offer the confirmed ones by name: for this email (`senderId`, nothing else changes) or as the app's sender. Never pick one yourself.
41
+ - Changing the app's sender also changes the address its sign-in emails come from. Ask the user before you call `application_configure_email_sender`, and name both effects. A workspace whose plan has no custom senders is refused; then say that sending from their own address needs the Professional plan.
42
+ - Set `senderId` only when the user wants this email from a specific address, such as `billing@`. Pick it from `workspace_list_email_senders`, copied exactly. A sender the mail provider has not confirmed makes every send fail until it is confirmed. That confirmation is all an email action needs; the domain's DNS records (DKIM, Return-Path) are needed only before Studio lets the app pick the address as its own sender, and they help the emails stay out of spam.
42
43
 
43
44
  ## What it costs
44
45
 
45
- Every email costs **5 workflow credits per recipient**, from the workspace's monthly workflow credits, which its workflows spend too: 500 on Free (about 100 single-recipient emails), 2,500 on Basic (about 500), 10,000 on Pro (about 2,000), 25,000 on Business (about 5,000). The plan and the closing message say so in plain numbers, for example: "Each submission emails you, at 5 workflow credits per email — about 100 emails a month on the Free plan."
46
+ Every email costs **5 workflow credits per recipient**, from the workspace's monthly workflow credits, which its workflows spend too. You do not know the workspace's plan, so never name one or count how many emails it covers. The plan and the closing message give the cost and where to watch it, for example: "Each submission emails you, at 5 workflow credits per email. Track them in Settings → Billing → Workflow usage, where this app has its own row."
46
47
 
47
48
  Emails sent from the preview are real and charged, and the actions in the code reach the live app as soon as they are saved. Send at most one test email, and only when the user asks for one. Never send emails in a loop to try something out.
48
49
 
@@ -99,13 +100,13 @@ const submit = createServerFn({ method: 'POST' })
99
100
  - `subject` and `body`. Required. `body` is plain text or HTML. Escape what visitors typed before you put it into HTML.
100
101
  - `to`, `cc`, `bcc`, `replyTo`: each used only where the action does not fix it, and `cc` and `bcc` never when it fixes `to`. Each entry is one address. At most 50 recipients per email.
101
102
  - `attachments`: `{ name, url }` each, a file uploaded to this app (the Softr SDK `file-uploads` guidance); any other URL is refused with `INVALID_INPUT`. 10 MB in total.
102
- - `idempotencyKey`: the same key sends once, however often the call is repeated. Pass one derived from what the email is about, such as the record it announces, so a retried request never sends twice. At most 128 characters. Left out, every call is a new email.
103
+ - `idempotencyKey`: the same key sends once, however often the call is repeated. Pass one derived from what the email is about, such as the record it announces, so a retried request never sends twice. When nothing is saved first, the form makes one key per submission (`crypto.randomUUID()` when the person starts submitting), sends it with the request and reuses it when they retry. At most 128 characters. Left out, every call is a new email, so a retry after `TIMEOUT` or a double click sends twice.
103
104
 
104
105
  It answers `{ messageId }` once the email is sent. The answer never lists the recipients.
105
106
 
106
107
  ## Failure modes
107
108
 
108
- **Not handling `EmailError`.** `email.send` throws an `EmailError` with `status`, `code` and `message` when the email was not sent, and never retries by itself. Catch it, keep what the request already saved, and show the person a line they can act on: "Saved, but the email could not be sent." The codes:
109
+ **Not handling `EmailError`.** `email.send` throws an `EmailError`, a `StepError` from `@softr/sdk/server`, with `status`, `code` and `message` when the email was not sent, and never retries by itself. Catch it, keep what the request already saved, and show the person a line they can act on: "Saved, but the email could not be sent." The codes:
109
110
 
110
111
  | `code` | Meaning |
111
112
  | --- | --- |
@@ -113,6 +114,9 @@ It answers `{ messageId }` once the email is sent. The answer never lists the re
113
114
  | `INVALID_INPUT` | No subject or body, an entry or placeholder that is not exactly one address, more than 50 recipients, an attachment that is not one of the app's uploads, or an idempotency key over 128 characters |
114
115
  | `UNAVAILABLE` | Softr could not send it or could not be reached: no workflow credits left, Softr's address past its 30 emails today, an unconfirmed sender, or an outage |
115
116
  | `TIMEOUT` | No answer in time. It may still have been sent: retry only with the same key |
117
+ | `RATE_LIMITED` | The app sent too often in the last minute or day. Tell the person to try again later |
118
+
119
+ **Letting a mistyped address reach Softr.** An address the person typed (the reply-to of a contact form, a client's email) that is not one address is refused with `INVALID_INPUT`, which reads as a vague failure. Check its format in the validator and tell them which field is wrong.
116
120
 
117
121
  **Calling from client code.** `email` is server-only. Wrap the send in a `createServerFn` and call that from the form or the button.
118
122
 
@@ -7,7 +7,7 @@ description: >-
7
7
  metadata:
8
8
  type: sub-skill
9
9
  library: softr-sdk
10
- library_version: 0.0.93-alpha.1
10
+ library_version: 0.0.93-alpha.2
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.93-alpha.1
11
+ library_version: 0.0.93-alpha.2
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.93-alpha.1
10
+ library_version: 0.0.93-alpha.2
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.93-alpha.1
12
+ library_version: 0.0.93-alpha.2
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.93-alpha.1
12
+ library_version: 0.0.93-alpha.2
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.93-alpha.1
11
+ library_version: 0.0.93-alpha.2
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.93-alpha.1
12
+ library_version: 0.0.93-alpha.2
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.93-alpha.1
12
+ library_version: 0.0.93-alpha.2
13
13
  ---
14
14
 
15
15
  **Boundary.** `Audience` and `defineVisibility` 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.93-alpha.1
12
+ library_version: 0.0.93-alpha.2
13
13
  ---
14
14
 
15
15
  **Boundary.** `definePage`, `pageHead`, 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.93-alpha.1
12
+ library_version: 0.0.93-alpha.2
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.