@lowdefy/docs-content 6.0.0 → 6.1.0

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.
@@ -66,8 +66,9 @@ The `AgentChat` block renders a streaming AI chat interface. It connects to a [L
66
66
  | Property | Type | Default | Description |
67
67
  | --- | --- | --- | --- |
68
68
  | `agentId` | string | | __Required__ - The `id` of the agent to connect to. |
69
- | `conversationId` | string | | Active conversation ID. When this changes, messages are cleared. If left empty, the block auto-mints a stable id for the session and surfaces it via `onConversationStart`, so every turn posts a consistent id. App-supplied ids are always authoritative. |
70
- | `messages` | array | | Load messages externally. `undefined` = no sync, `null` = clear, array = load. |
69
+ | `conversationId` | string | | Active conversation ID. When this changes, any in-flight reply is aborted (so it stops emitting `onDataPart` and tool events into the page) and messages are cleared. If left empty, the block auto-mints a stable id for the session and surfaces it via `onConversationStart`, so every turn posts a consistent id. App-supplied ids are always authoritative. |
70
+ | `messages` | array | | Load messages externally. `undefined` = no sync, `null` = clear, array = load. Re-synced whenever the array's length, last message id or `conversationId` changes. |
71
+ | `loading` | boolean | `false` | The app is still loading this conversation's transcript (for example, reading it from a database after a conversation switch). While `true`, a skeleton replaces the message area, suggestions are hidden and the composer is disabled, so nothing can be sent into a conversation whose messages have not synced yet. Drive it from state alongside `messages`, for the window before the transcript has synced — not while a reply is streaming, since a disabled composer also hides the stop control. |
71
72
  | `urlQuery` | object | | Query parameters sent with each request. Available server-side via `_payload`. |
72
73
  | `sharedState` | object | | Two-way bridge between page state and the agent. See [Shared State](#shared-state). |
73
74
  | `height` | string | `'calc(100dvh - 170px)'` | CSS height of the chat container. Only applies when `display` is `'inline'`. |
@@ -102,6 +102,37 @@ If one action in the chain of event actions fails by throwing an error, the acti
102
102
  # ...
103
103
  ```
104
104
 
105
+ Inside the `catch` list, the [`_error`](/_error) operator returns the error that sent the event there — the error of the failed action, or of a control whose operators failed. Outside a `catch` list it returns `null`. An error from the server carries the message "Something went wrong." unless the endpoint author wrote it, and keeps its `code` and `statusCode`, so a `catch` list can branch on them. An error raised in the browser keeps its real message, and carries `actionId`, the id of the action that failed.
106
+
107
+ ###### Branch on the error in a catch list:
108
+ ```yaml
109
+ events:
110
+ onClick:
111
+ try:
112
+ - id: save
113
+ type: Request
114
+ params: save_customer
115
+ messages:
116
+ error: false
117
+ catch:
118
+ - :if:
119
+ _eq:
120
+ - _error: statusCode
121
+ - 409
122
+ :then:
123
+ - id: conflict_message
124
+ type: DisplayMessage
125
+ params:
126
+ status: warning
127
+ content: Someone else changed this customer. Reload the page and try again.
128
+ :else:
129
+ - id: error_message
130
+ type: DisplayMessage
131
+ params:
132
+ status: error
133
+ content: Could not save the customer.
134
+ ```
135
+
105
136
  ## Control flow in action lists
106
137
 
107
138
  An item in an action list is either an action (`id` and `type`) or a control. Action lists support the routine controls [`:if`](/:if), [`:switch`](/:switch) and [`:return`](/:return):
@@ -128,7 +128,7 @@ The page-shell blocks ([`PageHeaderMenu`](/PageHeaderMenu), [`PageSidebarLayout`
128
128
 
129
129
  ## Built-in framework strings
130
130
 
131
- The framework emits a few user-visible strings (action loading toasts, validation summaries, popup-blocker warnings, the global error page). They live in a built-in catalog and surface as English by default. Override any of them by adding the same key under `config.i18n.messages.{locale}`:
131
+ The framework emits a few user-visible strings (action loading toasts, validation summaries, popup-blocker warnings, the global error page, the generic server error message). They live in a built-in catalog and surface as English by default. Override any of them by adding the same key under `config.i18n.messages.{locale}`:
132
132
 
133
133
  ```yaml
134
134
  config:
@@ -147,6 +147,7 @@ config:
147
147
  errorPage.name: 'Fehler'
148
148
  errorPage.message: 'Ein Fehler ist aufgetreten.'
149
149
  errorPage.homeLink: 'Zurück zur Startseite'
150
+ server.genericError: 'Etwas ist schiefgelaufen.'
150
151
  ```
151
152
 
152
153
  **Built-in keys you can override:**
@@ -164,6 +165,9 @@ config:
164
165
  | `errorPage.name` | `Error` | Full-page error fallback heading |
165
166
  | `errorPage.message` | `An error has occurred.` | Full-page error fallback body |
166
167
  | `errorPage.homeLink` | `Return to home page` | Full-page error fallback link |
168
+ | `server.genericError` | `Something went wrong.` | Message of any server error that is not an author error or an auth refusal |
169
+
170
+ `server.genericError` is the message users see for any server error that is not an author error (`:throw`, `:reject`, a failed `ValidateSchema` step, a plugin's `UserError`) or an authentication refusal. The server translates it for the request's locale, the same way in development and production, and it is the message that `_actions`, `_request_details` and [`_error`](/_error) read in page config. To show something else for one action, set `messages.error` on the action to override its error toast. To show the real message, read it with [`_error`](/_error) inside the endpoint's `:catch` and send it on, for example `:throw: { _error: message }`.
167
171
 
168
172
  ## Localizing block defaults
169
173
 
@@ -41,7 +41,15 @@ Find the more detailed block schema [here](/blocks).
41
41
 
42
42
  The config object has the following properties:
43
43
 
44
- - `cron: object`: Declare the deployment environments scheduled endpoints run in (`cron.environments`), so that every environment's crons are registered on the Vercel production deployment and forwarded from there. See [Scheduled Endpoints](/api).
44
+ - `environments: object`: The deployment environments of the app, keyed by name (for example `prod`, `staging`, `local`). Each build is for one of them, named by the `LOWDEFY_ENVIRONMENT` environment variable, and the current environment supplies the defaults for everything environment-specific. See [Deployment environments](/deployment-environments). Each environment has:
45
+ - `url: string`: The deployment origin, for example `https://staging.example.com`. Default for the auth base URL (`AUTH_URL`), notification links and logos (`RenderNotification` `serverUrl`), and where crons are forwarded to.
46
+ - `cron.secret: string`: The **name** of the Lowdefy secret holding this environment's `CRON_SECRET` — a plain string, not a `_secret` operator (environment settings are resolved at build time, where `_secret` is not evaluated). The value is read at runtime from `LOWDEFY_SECRET_<name>` on the deployment that forwards. An environment with a `cron.secret` has its schedules forwarded to it from the environment Vercel fires crons on.
47
+ - `cron.enabled: boolean`: Set `false` to register no cron jobs for this environment. Defaults to `true`.
48
+ - `email.filter: object`: The delivery filter (`replaceAddress`, `allowlist`, `regex`) applied to every `SMTPMailSend` and `SendGridMailSend` request in this environment, unless the connection sets its own `filter` (`filter: false` on a connection turns filtering off).
49
+ - `email.enabled: boolean`: Set `false` to send no email from `SMTPMailSend` and `SendGridMailSend` requests in this environment (auth emails still send). Defaults to `true`.
50
+ - `sentry.enabled: boolean`: Set `false` to turn Sentry off, server and client, in this environment. Defaults to `true`.
51
+ - `guards: object`: Pin secrets and environment variables to a pattern kept in config. `guards.secrets` maps a Lowdefy secret name (`LOWDEFY_SECRET_<name>`) and `guards.env` an environment variable name to a regular expression; in this environment the build fails unless each value matches (a missing value fails too), so changing a guarded value also takes a config change. See [Guards](/deployment-environments).
52
+ - `environment: string`: The name of the environment this build is for, used when the `LOWDEFY_ENVIRONMENT` environment variable is not set; the variable always wins. When `environments` is declared it must be one of its names. Server log lines carry it as `environment`, `_app: environment` reads it, and Sentry reports under it unless `logger.sentry.environment` is set.
45
53
  - `basePath: string`: Set the base path to serve the Lowdefy application from. This will route all pages under `https://example.com/<base-path>/<page-id>` instead of the default `https://example.com/<page-id>`. The basePath value must start with "/".
46
54
  - `homePageId: string`: The id of the page that will load when a visitor navigates to the home route, in other words, when the visitor navigates to `yourdomain.com` or `yourdomain.com/`.
47
55
  - `requestTimeout: number`: Maximum time in milliseconds a request may run before the server returns a timeout. This protects against requests that hang on an upstream call (database, SMTP, an external API) running all the way to the host's function limit — important on serverless platforms that bill by execution duration. Defaults to `30000` (30 seconds). Set to `0` to disable. Agent streaming routes are exempt, since they are long-lived by design.
@@ -41,6 +41,7 @@ Request types:
41
41
  - `replaceAddress: string`: Redirect all mail to this single address. When set, it takes precedence — every message goes to this address only, with `cc` and `bcc` dropped.
42
42
  - `allowlist: string[]`: A list of domains. Only recipients whose domain is in the list are sent to.
43
43
  - `regex: string`: A regular expression. Only recipient addresses matching the pattern are sent to.
44
+ When the connection sets no `filter`, or every field of it resolves to `null`, the current environment's `email.filter` from [`config.environments`](/lowdefy-schema) applies instead. A connection's own `filter` wins when it sets at least one field, and `filter: false` turns filtering off entirely, the environment's too. With `email.enabled: false` on the current environment, mail requests send nothing and report each message as `{ disabled: true }`.
44
45
 
45
46
  ### SendGridMailSend
46
47
 
@@ -75,6 +75,20 @@ The connection's `filter` is applied to every message before it is sent — from
75
75
  - `allowlist: string[]`: A list of domains. Only recipients whose domain is in the list are sent to; others are dropped. When every `to` recipient is filtered out, the message is not sent.
76
76
  - `regex: string`: A regular expression. Only recipient addresses matching the pattern are sent to.
77
77
 
78
+ When the connection sets no `filter`, or every field of it resolves to `null`, the current environment's `email.filter` from [`config.environments`](/lowdefy-schema) applies instead, so the per-environment rule lives in one place for every mail connection. A connection's own `filter` wins when it sets at least one field, and `filter: false` turns filtering off entirely — the environment's too — for mail that must reach its real recipient in every environment (an invite, say). Auth emails are never filtered. With `email.enabled: false` on the current environment, mail requests send nothing and report each message as `{ disabled: true }`.
79
+
80
+ ```yaml
81
+ config:
82
+ environments:
83
+ prod:
84
+ url: https://app.example.com
85
+ staging:
86
+ url: https://staging.example.com
87
+ email:
88
+ filter:
89
+ replaceAddress: team+staging@example.com
90
+ ```
91
+
78
92
  `allowlist` and `regex` combine — a recipient must satisfy both to receive mail.
79
93
 
80
94
  ### Examples
@@ -1,7 +1,7 @@
1
1
  # :reject
2
2
 
3
3
  ```
4
- ({:reject: string, :cause: any}): void
4
+ ({:reject: string | Error, :cause: any}): void
5
5
  ```
6
6
 
7
7
  The `:reject` control is used to return a user-friendly error to the client when validation fails or business rules are violated.
@@ -9,9 +9,11 @@ Unlike [`:throw`](/:throw), which indicates a system error, `:reject` represents
9
9
  The control immediately stops routine execution and returns with a `"reject"` status.
10
10
  Importantly, `:reject` does not trigger `:catch` blocks in [`:try`](/:try) statements, making it ideal for handling validation and business logic errors separately from system errors.
11
11
 
12
+ `:reject` also takes an Error as its message, such as the error a `:catch` caught: `:reject: { _error: true }`. The reject message is then "Something went wrong.", unless the error was a `UserError` or an authentication error, whose message is kept. To reject with the caught error's real message, name it: `:reject: { _error: message }`. See [`_error`](/_error).
13
+
12
14
  #### Keys
13
15
 
14
- - `:reject: string`: __Required__ - The error message that will be returned in the response object of the API call result.
16
+ - `:reject: string | Error`: __Required__ - The error message that will be returned in the response object of the API call result, or an Error whose message to use.
15
17
  - `:cause: any`: Additional metadata that will be returned with the error message.
16
18
 
17
19
  #### Examples
@@ -1,7 +1,7 @@
1
1
  # :throw
2
2
 
3
3
  ```
4
- ({:throw: string, :cause: any}): void
4
+ ({:throw: string | Error, :cause: any}): void
5
5
  ```
6
6
 
7
7
  The `:throw` control creates a system error that immediately stops routine execution and returns with an "error" status.
@@ -9,13 +9,40 @@ Unlike [`:reject`](/:reject), which handles expected user errors, `:throw` is de
9
9
  The key difference is that `:throw` can be caught and handled by [`:try`](/:try)/`:catch` blocks, while [`:reject`](/:reject) cannot.
10
10
  This makes `:throw` ideal for recoverable system errors where you might want to implement fallback logic.
11
11
 
12
+ `:throw` also takes an Error as its message, such as the error a `:catch` caught: `:throw: { _error: true }`. The error is rethrown as it is, with its own class and cause, and `:cause` is ignored. The client receives it with the message "Something went wrong.", keeping its `code` and `statusCode`, unless it was a `UserError`, whose message is kept. To show the user the caught error's real message, name it: `:throw: { _error: message }`. See [`_error`](/_error).
13
+
12
14
  #### Keys
13
15
 
14
- - `:throw: string`: __Required__ - The error message that will be returned in the response object of the API call result.
16
+ - `:throw: string | Error`: __Required__ - The error message that will be returned in the response object of the API call result, or an Error to rethrow.
15
17
  - `:cause: any`: Additional metadata that will be returned with the error message.
16
18
 
17
19
  #### Examples
18
20
 
21
+ ###### Rethrow a caught error
22
+ ```yaml
23
+ - :try:
24
+ - id: update_stock
25
+ type: MongoDBUpdateOne
26
+ connectionId: inventory
27
+ properties:
28
+ filter:
29
+ sku:
30
+ _payload: sku
31
+ update:
32
+ $inc:
33
+ quantity:
34
+ _payload: change
35
+ :catch:
36
+ - :log:
37
+ message: Stock update failed
38
+ reason:
39
+ _error: message
40
+ :level: error
41
+ - :throw:
42
+ _error: true
43
+ ```
44
+ The `:catch` logs the real message, then rethrows the error. The client receives it with the message "Something went wrong.", with the error's `code` and `statusCode`.
45
+
19
46
  ###### Throw on invalid status
20
47
  ```yaml
21
48
  - :switch:
@@ -7,6 +7,7 @@
7
7
  If one control in the routine chain defined in the `:try` key fails by throwing an error, the steps in the list following the failed control will not be executed.
8
8
  To handle any errors thrown by a routine element, a routine can be provided using `catch` key.
9
9
  The routine defined using the `:finally` key will be run regardless of whether or not the routine throws an error.
10
+ Inside the `:catch` routine, the [`_error`](/_error) operator returns the error that was caught, so the routine can branch on its `statusCode` or `code`, or read its `message`. `_error` returns the error of the innermost `:catch` that encloses it, and `null` outside a `:catch`.
10
11
 
11
12
  #### Keys
12
13
 
@@ -16,6 +17,25 @@ The routine defined using the `:finally` key will be run regardless of whether o
16
17
 
17
18
  #### Examples
18
19
 
20
+ ###### Branch on the caught error
21
+ ```yaml
22
+ - :try:
23
+ - id: get_customer
24
+ type: AxiosHttp
25
+ connectionId: crm
26
+ properties:
27
+ url: /customers
28
+ params:
29
+ id:
30
+ _payload: id
31
+ :catch:
32
+ - :if:
33
+ _eq: [{ _error: statusCode }, 404]
34
+ :then:
35
+ - :reject: Customer not found
36
+ - :throw: Customer lookup failed
37
+ ```
38
+
19
39
  ###### Try external API with fallback
20
40
  ```yaml
21
41
  - :try:
@@ -0,0 +1,143 @@
1
+ # Deployment environments
2
+
3
+ Most apps run as more than one deployment: production, a staging copy, a developer's machine. Everything that differs between them is declared once, under `config.environments`, and each deployment says which environment it is with the `LOWDEFY_ENVIRONMENT` environment variable. Lowdefy then takes every environment-specific default from the **current environment** — the app URL, crons, the email delivery filter, Sentry, and the feature switches — so none of them needs its own variable or operator chain.
4
+
5
+ ```yaml
6
+ # lowdefy.yaml
7
+ config:
8
+ environments:
9
+ local:
10
+ email:
11
+ filter:
12
+ replaceAddress: team+local@example.com
13
+ staging:
14
+ url: https://staging.example.com
15
+ cron:
16
+ secret: STAGING_CRON_SECRET
17
+ email:
18
+ filter:
19
+ replaceAddress: team+staging@example.com
20
+ prod:
21
+ url: https://app.example.com
22
+ guards:
23
+ secrets:
24
+ MONGODB_URI: 'acme-prod\.a1b2c\.mongodb\.net'
25
+ ```
26
+
27
+ Then set `LOWDEFY_ENVIRONMENT=prod` on the production deployment, `LOWDEFY_ENVIRONMENT=staging` on staging, and `LOWDEFY_ENVIRONMENT=local` in your local `.env`.
28
+
29
+ ## The current environment
30
+
31
+ `LOWDEFY_ENVIRONMENT` is read **at build time**, so set it wherever the app is built (on Vercel, as an Environment Variable of each Vercel environment). `config.environment` can name the current environment in config instead, for a deployment that sets no `LOWDEFY_ENVIRONMENT`; the variable always wins, so a literal committed to config never forces every deployment into one environment.
32
+
33
+ - With `config.environments` declared, the current environment must be one of its names — a typo fails the build.
34
+ - With `config.environments` declared, a production build (`lowdefy build`) fails when no current environment is set: no environment setting would apply — no email filter, no forwarded crons, no guards. The dev server warns and runs with no environment setting.
35
+ - Environment names may contain `A-Z`, `a-z`, `0-9`, `-` and `_`. `default` is reserved.
36
+
37
+ ## Environment settings
38
+
39
+ Every setting is optional, and every feature is **on by default**: only an explicit `enabled: false` switches one off.
40
+
41
+ | Setting | Default | What it does |
42
+ | --- | --- | --- |
43
+ | `url` | none | The deployment origin, e.g. `https://staging.example.com`. |
44
+ | `cron.secret` | none | The name of the Lowdefy secret holding this environment's `CRON_SECRET`; makes this environment a cron forwarding target. |
45
+ | `cron.enabled` | `true` | `false`: no cron jobs registered or forwarded for this environment. |
46
+ | `email.filter` | none | Delivery filter (`replaceAddress`, `allowlist`, `regex`) for mail requests. |
47
+ | `email.enabled` | `true` | `false`: mail requests send nothing. |
48
+ | `sentry.enabled` | `true` | `false`: Sentry off, server and client. |
49
+ | `guards.secrets`, `guards.env` | none | Regular expressions the named secrets and environment variables must match, or the build fails. |
50
+
51
+ ## URL
52
+
53
+ The current environment's `url` is the default for:
54
+
55
+ - the auth base URL (`AUTH_URL`) — sign-in callbacks and auth links;
56
+ - the `serverUrl` of `RenderNotification` steps — the links and logo in notification emails;
57
+ - where crons are forwarded to, when another environment forwards to this one.
58
+
59
+ An explicit `AUTH_URL` / `NEXTAUTH_URL` or `serverUrl` still wins. The dev server (`lowdefy dev`) never uses the current environment's `url`: auth and notification links use the request origin, the local app, even when your local `.env` names a deployed environment. Production never uses the request origin, since a spoofed `Host` header would then steer the links in outgoing mail.
60
+
61
+ Every deployment that names an environment is taken to be served at its `url`. Preview deployments that should not sign users in on the staging host need their own environment, e.g. a `preview` environment with no `url` and the same `email.filter` and guards as staging.
62
+
63
+ ## Crons
64
+
65
+ Vercel fires cron jobs only on the **production** deployment, so the schedules of other environments are registered there too and forwarded:
66
+
67
+ - The current environment registers its own schedules as `/api/cron/<endpointId>` jobs.
68
+ - An environment with a `cron.secret` is a **forwarding target**. The environment without one (production) also registers the target's schedules as `/api/cron-forward/<environment>/<endpointId>` jobs; when one fires, it pings the target's own `/api/cron/<endpointId>` at its `url` with the target's `CRON_SECRET` and answers immediately.
69
+ - `cron.secret` is the **name** of a Lowdefy secret — a plain string such as `STAGING_CRON_SECRET`, not a `_secret` operator (environment settings are resolved at build time, where `_secret` is not evaluated). Production reads the value at runtime from `LOWDEFY_SECRET_STAGING_CRON_SECRET`, so set that variable on the production deployment to staging's `CRON_SECRET`.
70
+ - `cron.enabled: false` registers nothing for that environment, and production does not forward to it.
71
+
72
+ Endpoint `schedules` can be keyed by environment name, with a `default` for the rest and `[]` to turn an endpoint off in one environment — see [Schedules per environment](/lowdefy-api). The Vercel side (securing the routes, plan limits) is covered in [Deploy with Vercel](/vercel).
73
+
74
+ ## Email
75
+
76
+ - **`email.filter`** is applied to every `SMTPMailSend` and `SendGridMailSend` request whose connection sets no `filter` of its own, or a filter whose fields all resolve to `null` (a `_secret` that is not set on this deployment, say). Put a catch-all `replaceAddress` on every non-production environment and no environment can email real users by accident.
77
+ - A connection's own `filter` wins when it sets at least one field, and **`filter: false`** on a connection turns filtering off entirely, the environment's too — for mail that must reach its real recipient everywhere, such as an invite.
78
+ - **`email.enabled: false`** sends nothing: each message reports `{ messageId: null, to: null, disabled: true }`.
79
+ - Auth emails (magic links, verification, invitations sent by the auth engine) never go through a mail request, so neither setting affects them — sign-in keeps working in every environment.
80
+
81
+ See the [SMTP](/SMTP) and [SendGridMail](/SendGridMail) connections for the filter fields.
82
+
83
+ ## Logs and Sentry
84
+
85
+ - Every server log line carries `environment` next to `app_name`, `app_version` and `git_sha`, so one log drain can hold every environment. Logging cannot be switched off.
86
+ - Sentry reports under the environment name unless `logger.sentry.environment` is set, and `sentry.enabled: false` turns it off on the server and in the browser.
87
+
88
+ ## Guards
89
+
90
+ Guards pin an environment's secrets and environment variables to a pattern kept in config — a second factor for deploy-time configuration. In the current environment the build fails before anything deploys unless every guarded value matches its regular expression; an unset value fails too. The production server checks the same guards again when it starts, against the variables it runs with, and refuses to start on a mismatch — an image built once can be started with different variables. Changing the production database then takes both a new variable value **and** a reviewed config change.
91
+
92
+ ```yaml
93
+ config:
94
+ environments:
95
+ prod:
96
+ url: https://app.example.com
97
+ guards:
98
+ secrets: # Lowdefy secrets, by name (LOWDEFY_SECRET_<name>)
99
+ MONGODB_URI: 'acme-prod\.a1b2c\.mongodb\.net'
100
+ env: # any environment variable
101
+ AUTH_URL: '^https://app\.example\.com$'
102
+ ```
103
+
104
+ - Patterns match anywhere in the value, so a distinctive substring is enough; add `^` and `$` to pin the whole value. A pattern is committed with your config, so never put the sensitive part of a secret in it — a host name or database name is enough.
105
+ - Only the current environment's guards are checked. Every environment's patterns are validated, so a broken pattern fails the build that introduces it. `secrets` and `env` are the only guard kinds; any other key (a `secret:` typo, say) fails the build.
106
+ - A failure names the variable and never prints its value.
107
+ - Guards need the real values at build time and at startup. On Vercel, Environment Variables are available to both; a container build that only injects secrets when the container starts sees them as unset and fails.
108
+
109
+ ## What reaches the browser
110
+
111
+ Environment settings stay on the server. The browser only learns the current environment's **name** and the list of features it switches off, through the app metadata:
112
+
113
+ - `_app: environment` — the current environment's name.
114
+ - `_app: disabled` — the switched-off features, e.g. `[email, sentry]`.
115
+
116
+ Other environments' settings, URLs, secret names, email filters and guards never reach the client bundle. Only the current environment's guards are kept in the build output, in the server-only `config.json`, for the startup check.
117
+
118
+ ## Upgrading from `config.cron`
119
+
120
+ Lowdefy 6.0 declared cron environments under `config.cron.environments`. That key is replaced, and a build that still has it fails with the migration:
121
+
122
+ ```yaml
123
+ # Before
124
+ config:
125
+ cron:
126
+ environments:
127
+ production: {}
128
+ staging:
129
+ url: https://staging.example.com
130
+ secret: STAGING_CRON_SECRET
131
+
132
+ # After
133
+ config:
134
+ environments:
135
+ production:
136
+ url: https://app.example.com
137
+ staging:
138
+ url: https://staging.example.com
139
+ cron:
140
+ secret: STAGING_CRON_SECRET
141
+ ```
142
+
143
+ Move each `secret` to `cron.secret` and `enabled` to `cron.enabled`, give production its `url`, and set `LOWDEFY_ENVIRONMENT` on each deployment.
@@ -123,7 +123,8 @@ The server is configured entirely through environment variables at runtime — t
123
123
  | `PORT` | Port the server listens on (default `3000`). |
124
124
  | `LOWDEFY_SECRET_*` | App secrets, accessed with the [`_secret`](/_secret) operator. |
125
125
  | `AUTH_SECRET` | Required when using [Lowdefy Auth](/users-introduction). |
126
- | `AUTH_URL` | The deployed app URL, required for OAuth providers. |
126
+ | `LOWDEFY_ENVIRONMENT` | Read at build time: the environment this image is built for, one of `config.environments`. |
127
+ | `AUTH_URL` | The deployed app URL, required for OAuth providers. Defaults to the `url` of the current environment in `config.environments`. |
127
128
  | `CRON_SECRET` | Bearer token protecting `/api/cron/*` and `/api/detached/*` endpoints. |
128
129
  | `LOWDEFY_LOG_LEVEL` | Server log level: `debug`, `info` (default), `warn`, `error`. |
129
130
  | `SENTRY_DSN` | Enables Sentry error reporting when set. |
@@ -34,7 +34,7 @@ Files in your app's `public/` directory (favicon, icons, images, `manifest.webma
34
34
 
35
35
  ###### Secrets and environment variables
36
36
 
37
- Secrets can be set in the Environment Variables settings section by creating environment variables prefixed with `LOWDEFY_SECRET_`. Use `AUTH_SECRET` (and `AUTH_URL` for OAuth) for authentication. Different secrets can be set for production and preview deployments.
37
+ Secrets can be set in the Environment Variables settings section by creating environment variables prefixed with `LOWDEFY_SECRET_`. Use `AUTH_SECRET` (and `AUTH_URL` for OAuth, which defaults to the current environment's `url` — see [Deployment environments](#deployment-environments)) for authentication. Different secrets can be set for production and preview deployments.
38
38
 
39
39
  ###### Function settings
40
40
 
@@ -80,33 +80,28 @@ Each schedule becomes a cron job pointing at `/api/cron/<endpointId>`. When it f
80
80
  - **Plan limits:** Vercel **Hobby** only allows daily crons (a sub-daily expression fails the deployment); **Pro**/**Enterprise** allow per-minute. Up to 100 cron jobs per project.
81
81
  - **Idempotency:** Vercel does not retry failed runs and delivery is best-effort (a run can be missed or delivered more than once). Design scheduled routines to be idempotent.
82
82
 
83
- ###### Crons for staging and other environments
83
+ ###### Deployment environments
84
84
 
85
- Vercel fires cron jobs only on the **production** deployment — a staging or preview deployment never runs its schedules on its own. Declare your environments under `config.cron` and Lowdefy registers every environment's schedules on production; when one for another environment fires, production forwards it to that environment's own `/api/cron/<endpointId>` and answers Vercel immediately (the ping is fire-and-forget, kept alive via the request context and bounded by `maxDuration`; its outcome is logged as `forward_scheduled_endpoint_done` / `_failed`).
85
+ Declare the app's environments once under `config.environments` and name the current one with the `LOWDEFY_ENVIRONMENT` Environment Variable — see [Deployment environments](/deployment-environments) for every setting (URL, crons, email, Sentry, feature switches, guards). On Vercel, add `LOWDEFY_ENVIRONMENT` to each Vercel environment (for example `prod` on Production and `staging` on the staging branch's Preview environment). Vercel Environment Variables are available at build time, which is when Lowdefy reads it and checks guards.
86
86
 
87
- ```yaml
88
- config:
89
- cron:
90
- environments:
91
- production: {} # no url: the deployment Vercel fires crons on
92
- staging:
93
- url: https://staging.example.com
94
- secret: STAGING_CRON_SECRET
95
- ```
87
+ ###### Crons for staging and other environments
88
+
89
+ Vercel fires cron jobs only on the **production** deployment — a staging or preview deployment never runs its schedules on its own. Give an environment a `cron.secret` and the environment Vercel fires crons on registers that environment's schedules too; when one fires, production forwards it to that environment's own `/api/cron/<endpointId>` at its `url` and answers Vercel immediately (the ping is fire-and-forget, kept alive via the request context and bounded by `maxDuration`; its outcome is logged as `forward_scheduled_endpoint_done` / `_failed`).
96
90
 
97
- - **One host environment:** exactly one environment has no `url` — the deployment whose crons Vercel fires. Every other environment needs its `url` (deployment origin) and a `secret`: the Lowdefy secret name holding that environment's own `CRON_SECRET`. Set `enabled: false` on an environment to register no crons for it.
91
+ - **Forwarded environments:** an environment with a `cron.secret` is forwarded to, so it must have a `url`. `cron.secret` is the **name** of the Lowdefy secret holding that environment's own `CRON_SECRET` — a plain string such as `STAGING_CRON_SECRET`, not `{ _secret: ... }`: environment settings are resolved at build time, and the forward route reads the value at runtime from `LOWDEFY_SECRET_<name>`. Set `cron.enabled: false` on an environment to register no crons for it.
98
92
  - **Secrets on production:** for each forwarded environment add an Environment Variable `LOWDEFY_SECRET_<secret name>` (for example `LOWDEFY_SECRET_STAGING_CRON_SECRET`) to the production deployment, with that environment's `CRON_SECRET` as value. The forward route `/api/cron-forward/*` itself is secured by production's own `CRON_SECRET` like every cron route, and fails closed when the environment's secret is missing.
99
- - **Per-environment schedules:** key an endpoint's `schedules` by environment name with an optional `default` (`staging: []` turns them off) — see [Schedules per environment](/api). The generated crons are `/api/cron/<endpointId>` for the host environment and `/api/cron-forward/<environment>/<endpointId>` for the others; all of them count toward the project's cron limit.
93
+ - **Per-environment schedules:** key an endpoint's `schedules` by environment name with an optional `default` (`staging: []` turns them off) — see [Schedules per environment](/lowdefy-api). The generated crons are `/api/cron/<endpointId>` for the current environment and `/api/cron-forward/<environment>/<endpointId>` for the forwarded ones; all of them count toward the project's cron limit.
100
94
  - **Plan limits still apply per project:** a per-minute staging schedule needs the same Pro plan as a per-minute production one.
95
+ - **Upgrading from 6.0:** `config.cron.environments` is replaced by `config.environments`; see [Upgrading from `config.cron`](/deployment-environments).
101
96
 
102
97
  ###### Background work: async endpoints and detached calls
103
98
 
104
- Two [API endpoint](/api) controls exist for work that should not hold up a response — they behave differently on Vercel:
99
+ Two [API endpoint](/lowdefy-api) controls exist for work that should not hold up a response — they behave differently on Vercel:
105
100
 
106
101
  - **`async: true` endpoints** respond `{ accepted: true }` immediately and run the routine in the background. On Vercel the invocation is kept alive via the platform request context (Fluid compute) until the routine settles — but it is still the *same* invocation, so the background work remains bounded by `maxDuration`. Raise `config.vercel.maxDuration` to cover your longest async routine.
107
102
  - **`detached: true` on `CallApi` steps** fire-and-forget the target endpoint through a `POST` to the deployment's own `/api/detached/<endpointId>` route, so the target runs in its **own** invocation with a fresh `maxDuration` budget. The route is secured by `CRON_SECRET` (same secret as cron, fail closed), and detached steps fail with a config error if it is not set. Delivery is at-most-once with no retry; targets run as system context (`_user` is `undefined`) and must be idempotent.
108
103
 
109
- See [Async Endpoints](/api) and [Detached Endpoint Calls](/api) in the API docs for full semantics.
104
+ See [Async Endpoints](/lowdefy-api) and [Detached Endpoint Calls](/lowdefy-api) in the API docs for full semantics.
110
105
 
111
106
  ###### Cost protection
112
107
 
@@ -0,0 +1,173 @@
1
+ # _error
2
+
3
+ ```
4
+ (key: string): any
5
+ (all: boolean): Error | null
6
+ (arguments: {
7
+ all?: boolean,
8
+ key?: string,
9
+ default?: any,
10
+ }): any
11
+ ```
12
+
13
+ The `_error` operator returns the error that is being handled:
14
+
15
+ - Inside a server [`:catch`](/:try) routine, it returns the error that sent the routine to the `:catch`.
16
+ - Inside a client [`catch`](/events-and-actions) action list, it returns the error that sent the event to the `catch` list.
17
+ - Outside a catch, it returns `null`, the way [`_item`](/_item) does outside a `:for`. Reading a key outside a catch returns `null`, or the `default` when one is given.
18
+
19
+ On the server, `_error` resolves lexically. It returns the error of the innermost enclosing `:catch`. After an inner `:try` finishes, the outer `:catch` reads its own error again. A `:finally` reads the error of the `:catch` that encloses its `:try`, or `null` if no `:catch` does. Each [`:parallel`](/:parallel) branch reads its own error. [`:reject`](/:reject) does not trigger `:catch`, so `_error` never holds a reject.
20
+
21
+ Dot notation is supported for reading fields of the error, such as `_error: statusCode` or `_error: cause.message`.
22
+
23
+ ##### The error on the server
24
+
25
+ Inside a `:catch`, `_error` is an Error rebuilt from the caught error with only these fields:
26
+
27
+ - `name`: The error class name, such as `RequestError`.
28
+ - `message`: The real error message, with the values of known secrets (`_secret` values, `CRON_SECRET` and `AUTH_SECRET`) replaced by `[REDACTED]`.
29
+ - `code`: The error's `code`, if it has one.
30
+ - `statusCode`: The error's HTTP status, if it has one.
31
+ - `handled`: `true` once the server has logged the error.
32
+ - `cause`: The error that caused this one, with the same fields, down the chain.
33
+
34
+ A caught `UserError` (from a [`:throw`](/:throw) or a plugin) also keeps its `:cause` and `metaData`. The error carries no `received`, `source`, `config`, `configKey`, `location` or `stack`.
35
+
36
+ ##### The error on the client
37
+
38
+ Inside a client `catch` list, `_error` is the error the browser holds:
39
+
40
+ - An error from the server (a failed `Request` or `CallAPI`) is the error as the browser received it: `name`, `message`, `code`, `statusCode` and `requestId`. The `message` is "Something went wrong." (the [`server.genericError`](/i18n) message) unless the author wrote it with `:throw` or `:reject`, it comes from a failed `ValidateSchema` step, or it is a plugin's `UserError`.
41
+ - An error raised in the browser (an action or operator that failed on the page) keeps its real message, and carries `actionId`, the id of the action that failed.
42
+
43
+ ##### Sending the error on
44
+
45
+ Inside the routine, `_error: message` and `_error: statusCode` read the real values. When a routine sends the whole error to the client, the client receives it with the generic message, keeping `name`, `code` and `statusCode`. This applies when the whole `_error` is used as a `:cause`, as a [`:throw`](/:throw) message, or inside a [`:return`](/:return) value. To show the user the real message, send it by name: `:throw: { _error: message }`.
46
+
47
+ Sent to a plugin as data, such as `body: { error: { _error: true } }` in an `AxiosHttp` request or a document in a MongoDB insert, the error records no message. Name the fields to send them:
48
+
49
+ ```yaml
50
+ error:
51
+ name:
52
+ _error: name
53
+ message:
54
+ _error: message
55
+ statusCode:
56
+ _error: statusCode
57
+ ```
58
+
59
+ ##### `code` and `statusCode`
60
+
61
+ `code` and `statusCode` follow one rule across connection libraries. `code` is the error's own `code`, such as `ERR_BAD_REQUEST` from AxiosHttp or `11000` from MongoDB. `statusCode` is the first number among the error's `statusCode`, `status` and `response.status`. A request error takes both from the error the connection threw, so `_error: statusCode` reads the HTTP status of a failed `AxiosHttp` request without reading the cause.
62
+
63
+ #### Arguments
64
+
65
+ ###### string
66
+ If the `_error` operator is called with a string argument, the value of that field of the error is returned. If the field is not found, or there is no error, `null` is returned. Dot notation is supported.
67
+
68
+ ###### boolean
69
+ If the `_error` operator is called with boolean argument `true`, the whole error is returned, or `null` outside a catch.
70
+
71
+ ###### object
72
+ - `all: boolean`: If `all` is set to `true`, the whole error is returned, or `null` outside a catch. One of `all` or `key` are required.
73
+ - `key: string`: The value of that field of the error is returned. If the field is not found, or there is no error, `null` or the specified default value is returned. Dot notation is supported. One of `all` or `key` are required.
74
+ - `default: any`: A value to return if the `key` is not found. By default, `null` is returned.
75
+
76
+ #### Examples
77
+
78
+ ###### Branch on the HTTP status in a server `:catch`:
79
+ ```yaml
80
+ - :try:
81
+ - id: get_customer
82
+ type: AxiosHttp
83
+ connectionId: crm
84
+ properties:
85
+ url: /customers
86
+ params:
87
+ id:
88
+ _payload: id
89
+ :catch:
90
+ - :if:
91
+ _eq: [{ _error: statusCode }, 404]
92
+ :then:
93
+ - :reject: Customer not found
94
+ - :throw: Customer lookup failed
95
+ ```
96
+ A `404` from the CRM rejects with a message for the user. Any other failure throws a new error, which the user sees as "Customer lookup failed".
97
+
98
+ ###### Rethrow with the real message:
99
+ ```yaml
100
+ - :try:
101
+ - id: charge
102
+ type: AxiosHttp
103
+ connectionId: payments
104
+ properties:
105
+ url: /charges
106
+ method: post
107
+ data:
108
+ amount:
109
+ _payload: amount
110
+ :catch:
111
+ - :throw:
112
+ _string.concat:
113
+ - 'Payment failed: '
114
+ - _error: message
115
+ ```
116
+ The user sees the real error message after `Payment failed: `. Without naming `message`, `:throw: { _error: true }` would show "Something went wrong.".
117
+
118
+ ###### Log the error with a default:
119
+ ```yaml
120
+ - :try:
121
+ - id: sync_orders
122
+ type: MongoDBInsertMany
123
+ connectionId: orders
124
+ properties:
125
+ docs:
126
+ _payload: orders
127
+ :catch:
128
+ - :log:
129
+ message: Order sync failed
130
+ code:
131
+ _error:
132
+ key: code
133
+ default: unknown
134
+ reason:
135
+ _error: message
136
+ :level: warn
137
+ - :return:
138
+ synced: false
139
+ ```
140
+
141
+ ###### Branch on the HTTP status in a client `catch` list:
142
+ ```yaml
143
+ - id: save_button
144
+ type: Button
145
+ properties:
146
+ title: Save
147
+ events:
148
+ onClick:
149
+ try:
150
+ - id: save
151
+ type: Request
152
+ params: save_customer
153
+ messages:
154
+ error: false
155
+ catch:
156
+ - :if:
157
+ _eq:
158
+ - _error: statusCode
159
+ - 409
160
+ :then:
161
+ - id: conflict_message
162
+ type: DisplayMessage
163
+ params:
164
+ status: warning
165
+ content: Someone else changed this customer. Reload the page and try again.
166
+ :else:
167
+ - id: error_message
168
+ type: DisplayMessage
169
+ params:
170
+ status: error
171
+ content: Could not save the customer.
172
+ ```
173
+ `messages.error: false` turns off the request's own error message, so the `catch` list decides what the user sees.
package/index.json CHANGED
@@ -235,6 +235,12 @@
235
235
  "section": "Deployment",
236
236
  "path": "content/deployment/vercel.md"
237
237
  },
238
+ {
239
+ "slug": "deployment/deployment-environments",
240
+ "title": "Deployment environments",
241
+ "section": "Deployment",
242
+ "path": "content/deployment/deployment-environments.md"
243
+ },
238
244
  {
239
245
  "slug": "plugins/plugins-introduction",
240
246
  "title": "Plugins",
@@ -1723,6 +1729,14 @@
1723
1729
  "kind": "operator",
1724
1730
  "typeName": "_eq"
1725
1731
  },
1732
+ {
1733
+ "slug": "operators/_error",
1734
+ "title": "_error",
1735
+ "section": "Operators",
1736
+ "path": "content/operators/_error.md",
1737
+ "kind": "operator",
1738
+ "typeName": "_error"
1739
+ },
1726
1740
  {
1727
1741
  "slug": "operators/_event",
1728
1742
  "title": "_event",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/docs-content",
3
- "version": "6.0.0",
3
+ "version": "6.1.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Lowdefy documentation extracted as markdown for AI coding agents.",
6
6
  "homepage": "https://lowdefy.com",