@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.
- package/content/agents/agentchat.md +3 -2
- package/content/concepts/events-and-actions.md +31 -0
- package/content/concepts/i18n.md +5 -1
- package/content/concepts/lowdefy-schema.md +9 -1
- package/content/connections/sendgridmail.md +1 -0
- package/content/connections/smtp.md +14 -0
- package/content/controls/reject.md +4 -2
- package/content/controls/throw.md +29 -2
- package/content/controls/try.md +20 -0
- package/content/deployment/deployment-environments.md +143 -0
- package/content/deployment/docker.md +2 -1
- package/content/deployment/vercel.md +11 -16
- package/content/operators/_error.md +173 -0
- package/index.json +14 -0
- package/package.json +1 -1
|
@@ -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):
|
package/content/concepts/i18n.md
CHANGED
|
@@ -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
|
-
- `
|
|
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:
|
package/content/controls/try.md
CHANGED
|
@@ -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
|
-
| `
|
|
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
|
-
######
|
|
83
|
+
###### Deployment environments
|
|
84
84
|
|
|
85
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
- **
|
|
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
|
|
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",
|