# What is Lodol? > Lodol runs your automations for you. Say what should happen and when, and Lodol does it every time, with your keys kept safe and every run on the record. Web page: https://docs.lodol.com/docs A Lodol **workflow** is a short list of steps that runs on its own. It starts when something happens in one of your apps, on a schedule, or when you start it, and then works through its steps the same way every time. There is no server to keep running, no script to maintain and no keys sitting in a file. This one workflow replaces a script, a cron job, a server and an alert. It starts itself when a payment comes in, uses your Stripe, Google and Slack connections without ever showing their keys, records every run step by step, and emails you if a run fails. The rest of these docs build on this example. ## Three ways to build a workflow You can also start workflows and read their results from your own code with the [developer API](https://docs.lodol.com/docs/api.md). ## The parts of a workflow | Part | What it is | | --- | --- | | [Trigger](https://docs.lodol.com/docs/concepts/triggers.md) | What starts the workflow: an event in an app, a schedule, or you. | | [Steps](https://docs.lodol.com/docs/concepts/steps-and-data.md) | What it does, in order: app actions such as **Append row**, and built-in steps such as **If** or **Request approval**. | | [Data](https://docs.lodol.com/docs/concepts/steps-and-data.md#every-step-hands-its-result-to-the-steps-after-it) | What each step produces, which later steps read with `@` references such as `@payment.amount`. | | [Inputs and outputs](https://docs.lodol.com/docs/concepts/inputs-and-outputs.md) | Values a workflow can be given when it starts, and values it hands back. | | [Runs](https://docs.lodol.com/docs/concepts/runs.md) | Each time the workflow runs, recorded step by step. | | [Connections and secrets](https://docs.lodol.com/docs/concepts/connections.md) | Your app accounts and API keys, kept encrypted and used by name. | ## Where to go next # Build with your AI tool > Connect Claude, ChatGPT, Cursor or any tool that supports MCP, then describe the automation you want. Your AI builds the workflow in Lodol, and Lodol runs it. Web page: https://docs.lodol.com/docs/build-with-ai Your AI tool is good at working out what an automation should do, but it can't keep one running. Connected to Lodol, it builds a workflow in your workspace instead of handing you a script, and Lodol runs that workflow on every trigger from then on, whether or not the AI tool is open. Lodol connects through **MCP**, the open standard AI tools use to plug into other software. You sign in with your Lodol account when you connect, so there is no API key to copy. ```txt title="Lodol's MCP server" https://mcp.lodol.com/mcp ``` ## Connect your tool [Add Lodol to Claude](https://claude.ai/customize/connectors?modal=add-custom-connector&connectorName=Lodol&connectorUrl=https%3A%2F%2Fmcp.lodol.com%2Fmcp). Claude opens its connector settings with Lodol filled in. Click **Add**, then **Connect**, and sign in to Lodol. Turn on **Developer mode** in ChatGPT's settings. Then, under **Plugins**, create a plugin with the server URL above, choose **OAuth**, and sign in to Lodol. [Add Lodol to Cursor](https://cursor.com/install-mcp?name=lodol&config=eyJ1cmwiOiJodHRwczovL21jcC5sb2RvbC5jb20vbWNwIn0%3D). Cursor opens with Lodol ready to install. Click **Install**, then **Authenticate**, and sign in to Lodol. Add the server from your terminal: ```bash claude mcp add --transport http lodol https://mcp.lodol.com/mcp ``` Then run `/mcp` in Claude Code and choose **lodol** to sign in. Add Lodol to `.vscode/mcp.json` in your project, or to your user settings: ```json title=".vscode/mcp.json" { "servers": { "lodol": { "type": "http", "url": "https://mcp.lodol.com/mcp" } } } ``` Start the server from the file and sign in to Lodol when VS Code asks. Add the server URL as a **remote MCP server** in Codex, Gemini CLI, Devin, Lovable, Replit, Zed, Cline or any other tool that supports MCP. Choose OAuth if the tool asks how to sign in; it then asks you to sign in to Lodol. When you connect, you choose the workspace your AI works in, and you can untick any permission you don't want it to have. ## Ask for a workflow Describe the automation the way you'd brief a colleague: what should start it, what should happen, and where things go. ```txt title="Try this in your AI tool" Use Lodol: when a Stripe payment comes in, add it to our revenue sheet and post it in #sales. ``` A few details make the first version right: - **Say what starts it.** "When a payment comes in" asks for a trigger; "every Monday at 9:00" asks for a schedule. See [Triggers and schedules](https://docs.lodol.com/docs/concepts/triggers.md). - **Name the places.** The sheet, the channel, the people to ask. Your AI leaves out anything it doesn't know rather than guessing, and tells you what to fill in. - **Change it the same way.** "Only post payments over $1,000" updates the workflow you already have. ## What happens next ### Your AI builds it and gives you a link The workflow is saved in your Lodol workspace. Lodol checks it as it's saved, so it only uses apps, actions and fields that exist, and your AI tells you anything it left for you to fill in, such as a sheet it wasn't given. ### Connect the apps it uses Open the link. Any app the workflow uses that isn't connected yet shows a **Connect** button at its step. This is a one-time setup per app; see [Connections and secrets](https://docs.lodol.com/docs/concepts/connections.md). ### Turn it on From then on the workflow runs on its trigger or schedule. Each run is recorded step by step, and you get an email if one fails. ## What your AI can do, and what it can't Connected to Lodol, your AI can: - find and explain the workflows in your workspace - build new workflows and change existing ones - run a workflow and read what happened in a run - work out why a run failed and fix the workflow It can't connect apps, see the values of your secrets or API keys, or manage members and billing. It can only do what your own [role](https://docs.lodol.com/docs/concepts/workflows.md#who-can-do-what) allows, and it can delete a workflow or a run only if you ticked **Delete workflows** or **Delete runs** when you connected it. Each change your AI saves becomes a new [version](https://docs.lodol.com/docs/concepts/workflows.md#every-change-is-a-version) you can roll back to. Saving a change pauses the workflow until it's turned back on, which your AI can do for you. ## What it costs Your AI tool only uses its own tokens while it builds or changes a workflow. Once the workflow exists, its runs come out of your Lodol plan, which counts each [run](https://docs.lodol.com/docs/concepts/runs.md) once however many steps it has, and no model is called unless a step asks for one. You can disconnect your AI at any time from the **Connect AI** page in Lodol. # Workflows > A workflow is a trigger followed by steps, read top to bottom. Lodol keeps every version of it, runs it when it should, and records what each run did. Web page: https://docs.lodol.com/docs/concepts/workflows A workflow is the thing you build in Lodol: a trigger that says when it runs, followed by the steps it takes, in order. Anyone in your workspace can open it and read what it does, the way you'd read a short checklist. ## A workflow reads top to bottom Lodol runs the steps in the order they're listed. A step can only use what came before it: the trigger's event and the results of earlier steps. Sections change that order in two ways: - An **If** section runs its steps only when its condition is true, with an optional **Otherwise** for when it isn't. See [Make decisions](https://docs.lodol.com/docs/guides/conditions.md). - A **Repeat** section runs its steps once for each item in a list, a set number of times, or while something is true. See [Repeat steps](https://docs.lodol.com/docs/guides/loops.md). A run ends when it reaches the last step, when a step fails, or when someone stops it. ## It starts in one of three ways Every workflow starts the same way each time: | Start | Runs when | Example | | --- | --- | --- | | An app trigger | Something happens in a connected app | A new Stripe payment, a new Slack message, a new form response | | A schedule | At the times you choose | Every 15 minutes, or every Monday at 9:00 | | On demand | You, your code or another workflow starts it | A monthly report you run for one client at a time | A workflow with a trigger or a schedule has to be turned on before it runs. [Triggers and schedules](https://docs.lodol.com/docs/concepts/triggers.md) covers all three. ## Every change is a version Each time a workflow is saved, Lodol keeps a new version. You can see what changed against the version that's running and roll back to any earlier one. Saving a change pauses a workflow that was on, until it's turned back on. A workflow keeps the same ID across all its versions, so links to it, and code that runs it through the [API](https://docs.lodol.com/docs/api/workflows.md), keep working after it's edited. Each [run](https://docs.lodol.com/docs/concepts/runs.md) records which version it ran. ## Private until you share it A new workflow is visible only inside your workspace. To show one to someone outside it, make it public: anyone with its preview link can then see how it works, and you can make it private again at any time. ## Who can do what What each member can do depends on their role in the workspace. The built-in roles are **Owner**, **Admin**, **Member** and **Viewer**: - **Viewers** can see workflows, runs and apps, but can't change anything. They suit people who only need to follow along. - Running workflows, deleting them and deleting runs are separate permissions, **Run workflows**, **Delete workflows** and **Delete runs**, which a role may or may not include. Deleting a workflow keeps its runs; only **Delete runs** removes them. - On the Pro and Enterprise plans you can create custom roles with exactly the permissions each member needs. The same limits apply to anything acting for a member: an [AI tool they connected](https://docs.lodol.com/docs/build-with-ai.md) and an [API key](https://docs.lodol.com/docs/api/authentication.md) they created can only do what their role allows. ## How many run at once Each plan sets how many workflows can be running at the same moment: 3 on Free, 10 on Builder and 40 on Pro, with a custom limit on Enterprise. A run you start through the API while you're at the limit is refused, so your code can retry; see [Limits](https://docs.lodol.com/docs/api/limits.md). [Pricing](https://www.lodol.com/pricing) has the current plans. ## Next # Triggers and schedules > A workflow starts when something happens in an app, at the times you schedule, or when you start it. Each start is one run. Web page: https://docs.lodol.com/docs/concepts/triggers Every workflow has one way to start, and it's the first thing in the workflow. Choose it by asking what the automation is for: | If the goal is… | Start it with | Example | | --- | --- | --- | | Reacting when something happens | An [app trigger](#app-triggers) | "When a Stripe payment comes in…" | | Doing something at regular times | A [schedule](#schedules) | "Every Monday at 9:00, send the pipeline digest." | | Doing something once, when you decide | [Nothing: run it on demand](#on-demand) | "Build this month's report for Acme." | ## App triggers An app trigger starts a run each time an event happens in a connected app: a new payment in Stripe, a new message in a Slack channel, a new email in Gmail, a new response to a form. The run receives the event, and later steps read its fields. Most triggers take **filters** that narrow which events start a run. **New Stripe payment** can be limited to one payment status; **New Slack message** watches the channel you pick and can be limited to one person or a keyword. **New Stripe payment** runs when a payment is *created*, which includes checkouts still waiting for the customer to pay. To log only completed payments, set its status filter to `succeeded`. ### What a trigger hands to the run Each trigger delivers a fixed set of fields. New Stripe payment, for example, delivers the payment's `amount`, `currency`, `status`, `customer`, `description` and when it was `created`. A step can only use what the event carries: the Gmail trigger delivers an email's sender, recipients, subject, date and body, but not its attachments. To work with those, add Gmail's [Download attachments](https://docs.lodol.com/docs/integrations/gmail.md#download-attachments) step. Each app's page lists its triggers, with their filters and the fields each one delivers. These apps have triggers: - [Airparser](https://docs.lodol.com/docs/integrations/airparser.md) - [Airtable](https://docs.lodol.com/docs/integrations/airtable.md) - [Box](https://docs.lodol.com/docs/integrations/box.md) - [Calendly](https://docs.lodol.com/docs/integrations/calendly.md) - [Canva](https://docs.lodol.com/docs/integrations/canva.md) - [ClickUp](https://docs.lodol.com/docs/integrations/clickup.md) - [Coda](https://docs.lodol.com/docs/integrations/coda.md) - [Discord](https://docs.lodol.com/docs/integrations/discord.md) - [Dropbox](https://docs.lodol.com/docs/integrations/dropbox.md) - [Eventbrite](https://docs.lodol.com/docs/integrations/eventbrite.md) - [Fillout](https://docs.lodol.com/docs/integrations/fillout.md) - [GitHub](https://docs.lodol.com/docs/integrations/github.md) - [Gmail](https://docs.lodol.com/docs/integrations/gmail.md) - [Google Calendar](https://docs.lodol.com/docs/integrations/google-calendar.md) - [Google Docs](https://docs.lodol.com/docs/integrations/google-docs.md) - [Google Drive](https://docs.lodol.com/docs/integrations/google-drive.md) - [Google Sheets](https://docs.lodol.com/docs/integrations/google-sheets.md) - [Linear](https://docs.lodol.com/docs/integrations/linear.md) - [Mercury](https://docs.lodol.com/docs/integrations/mercury.md) - [Microsoft Outlook](https://docs.lodol.com/docs/integrations/microsoft-outlook.md) - [Notion](https://docs.lodol.com/docs/integrations/notion.md) - [PayPal](https://docs.lodol.com/docs/integrations/paypal.md) - [QuickBooks Online](https://docs.lodol.com/docs/integrations/quickbooks.md) - [Rackspace Email](https://docs.lodol.com/docs/integrations/rackspace-email.md) - [Rackspace Email Admin](https://docs.lodol.com/docs/integrations/rackspace-email-admin.md) - [Slack](https://docs.lodol.com/docs/integrations/slack.md) - [Stripe](https://docs.lodol.com/docs/integrations/stripe.md) - [Telegram](https://docs.lodol.com/docs/integrations/telegram.md) - [Todoist](https://docs.lodol.com/docs/integrations/todoist.md) - [Trello](https://docs.lodol.com/docs/integrations/trello.md) - [Twitch](https://docs.lodol.com/docs/integrations/twitch.md) - [X (Twitter)](https://docs.lodol.com/docs/integrations/twitter.md) - [Zoom](https://docs.lodol.com/docs/integrations/zoom.md) For any other app, a [schedule](#schedules) can check for new records instead. ## Schedules A schedule starts a run at the times you choose. There are three kinds: - **Every few minutes, hours or days**, such as every 15 minutes. The shortest interval is one minute. - **At a time of day, on the days you choose**: every day at 8:00, every Monday, Wednesday and Friday at 17:30, or on the first of each month. - **Once**, at a date and time you set. Times follow the workflow owner's time zone unless you choose another one. ### Choosing between a trigger and a schedule If the goal is "when something happens", use the app's trigger. A schedule that checks for new records every hour does the same work up to an hour late, and has to work out which records it already handled. Use a schedule when the goal really is about time, like a weekly digest or a daily check, and keep two things in mind: - **Look back exactly one interval.** A daily run that reports on "yesterday" sees every day once. A run that only happens on weekdays never sees Saturday or Sunday, so look back to the last working day, or run every day. - **Don't overlap windows.** A daily run asking for "everything changed since yesterday" can see the same record on two days. End each window where the next run's begins. ## On demand A workflow with no trigger and no schedule runs only when something starts it: - **you**, from the workflow in Lodol - **your code**, through the [developer API](https://docs.lodol.com/docs/api/runs.md#run-a-workflow) - **another workflow**, with a [Run a workflow](https://docs.lodol.com/docs/guides/sub-workflows.md) step - **an AI agent**, which can use the workflow as one of its [tools](https://docs.lodol.com/docs/guides/ai-agents.md#tools) On-demand workflows can declare [inputs](https://docs.lodol.com/docs/concepts/inputs-and-outputs.md), such as which client to report on, so one workflow can be run for any of them. A workflow with a trigger or a schedule takes no inputs: everything it needs comes from the event or from its steps. ## Turning a workflow on A workflow with a trigger or a schedule runs only while it's turned on. Once it's on, each event, or each scheduled time, starts one run, until you turn it off or delete it. A workflow waiting for its trigger doesn't use any of your plan's runs. Saving a change to a workflow that's on pauses it until you turn it back on. ## Next # Steps and data > Steps are what a workflow does, in order. Each one hands its result to the steps after it, which read it with @ references. Web page: https://docs.lodol.com/docs/concepts/steps-and-data A step is one thing a workflow does: add a row, send a message, make a decision, wait for an approval. Steps run in order, and each can use the results of every step before it. ## Two kinds of steps **App actions** work in one of your apps through your connection to it, such as Google Sheets' **Append row** or Slack's **Send a message to a channel**. There are thousands of them; the [integrations directory](https://docs.lodol.com/docs/integrations.md) lists every one with its inputs and outputs. **Built-in steps** are Lodol's own. They decide which steps run, repeat steps, wait for people, call AI models, and work with data and files: | Built-in step | Use it to | | --- | --- | | [If and Otherwise](https://docs.lodol.com/docs/guides/conditions.md) | Run steps only when something is true | | [Repeat](https://docs.lodol.com/docs/guides/loops.md) | Run steps once per item in a list, or several times | | [Request approval](https://docs.lodol.com/docs/guides/approvals.md) | Pause until a person approves, rejects or replies | | [Run an AI agent](https://docs.lodol.com/docs/guides/ai-agents.md) | Have an AI read, research or decide, and hand back fields | | [Text](https://docs.lodol.com/docs/integrations/built-in/text.md), [Lists](https://docs.lodol.com/docs/integrations/built-in/lists.md), [Math](https://docs.lodol.com/docs/integrations/built-in/math.md), [Dates & Times](https://docs.lodol.com/docs/integrations/built-in/datetime.md) | Format, clean up, count and convert values | | [Files](https://docs.lodol.com/docs/integrations/built-in/files.md) | Read what's inside a file, make a CSV, Excel or PDF file, or resize an image | | [Stored values](https://docs.lodol.com/docs/guides/stored-values.md) | Remember a value from one run to the next | | [Wait](https://docs.lodol.com/docs/integrations/built-in/delay.md) | Pause for a while, or until a date and time | | [Run a workflow](https://docs.lodol.com/docs/guides/sub-workflows.md) | Use another workflow as a step | | [Call an API, Run Python code](https://docs.lodol.com/docs/guides/http-and-code.md) | Reach a service with no app in Lodol, or compute something no step covers | ## Every step hands its result to the steps after it The trigger's event and each step's result have a name. Later steps read them with an `@` reference: in the editor you type `@` and pick the value, and it appears as a single token in the field. In the Stripe example, the trigger's event is called `payment`, so the Slack step's **Message** can read: ```txt title="Send a message to a channel › Message" New payment: @payment.amount @payment.currency for @payment.description ``` Stripe sends `amount` in cents, so the finished workflow divides it by 100 and formats it first; see [Money can arrive in cents](#good-to-know-about-data). References reach as deep as the data goes: | Reference | Reads | | --- | --- | | `@payment.amount` | The `amount` field of the trigger's event | | `@lead.company.name` | A field inside a field | | `@sheet.rows` | A whole list, ready for a [Repeat](https://docs.lodol.com/docs/guides/loops.md) step | | `@sheet.rows[0]` | The first item of a list (`[-1]` is the last) | Every action's integration page lists its **Outputs**: the fields its result has. A step that returns a list puts it inside a field rather than returning the bare list: Google Sheets' **Read rows** returns its rows in `rows`, so a Repeat goes over `@sheet.rows`, not `@sheet`. ## Good to know about data Data from apps arrives the way the app stores it, which isn't always the way people read it. These are the cases that most often surprise people. Stripe and similar apps report amounts in the smallest unit of the currency: `12500` is $125.00. Divide by 100 before you compare the amount with a figure or show it to anyone, so a condition reads the way you'd say it ("is at least 1,000"). Calculations produce values like `6.234567891`. Before a number appears in an email or message, put it through **Round number**, or **Format number** with a style such as `thousands` or `usd`. There's no percent style: multiply the ratio by 100, round it, and write `%` after it. Apps send dates like `2026-10-06T14:30:00Z`. Use **Make date readable** before a date appears in a message ("Tuesday, October 6 at 2:30 PM"). To check whether a date falls within a window, such as "in the next 30 days", use **Difference between dates** and compare the number of days, rather than comparing the dates as text. "Paid" is not "paid". When you compare text that people type, like a status column in a sheet, put it through **Format text** as lowercase first, and compare it with lowercase text. To test for an empty cell, compare it with empty text. Spreadsheet cells and some money fields arrive as text. Comparing them with a number works, but adding to them joins the text instead (`0` + `12.50` gives `012.50`). Turn the text into a number with **Extract from text** set to extract a number. To total a column across many rows, use **Get one field from every item** and then **Calculate total**, which read numeric text as numbers and skip blanks. Google Sheets' **Read rows** returns each row keyed by its header, so you can read `@row.Email`. Its new-row trigger keeps the columns under `fields`: read `@row.fields.Amount`. Avoid positions like `@row[2]`: if someone reorders the columns, the step quietly reads the wrong one. ## Workspace values Besides step results, a step can read values your workspace keeps, such as a base URL every workflow shares. Write them as `@values.NAME`, for example `@values.BASE_URL`. Keys and passwords don't belong in values: store them as [secrets](https://docs.lodol.com/docs/concepts/connections.md#secrets), which stay encrypted and hidden. ## Next # Inputs and outputs > Inputs let one workflow run for many cases, such as a report for any client. Outputs are what it hands back to a workflow that runs it. Web page: https://docs.lodol.com/docs/concepts/inputs-and-outputs An **input** is a value a workflow asks for when it starts, so the same workflow can run for different cases: a monthly report that takes the client and the month, or an onboarding workflow that takes the new hire's email. An **output** is a value a workflow hands back when it finishes, for the workflow that ran it. Inputs are for workflows you [run on demand](https://docs.lodol.com/docs/concepts/triggers.md#on-demand). A workflow with a trigger or a schedule takes no inputs: its event, or its steps, provide everything it needs. ## Declaring inputs Each input has a name, a type, and whether every run must give it a value: | Setting | What it means | | --- | --- | | Name | What steps call it, and what callers pass the value under, such as `client` | | Type | `text`, `number`, `boolean`, `list` or `object` | | Required | Whether every run must be given a value | | Default | The value an optional input takes when a run leaves it out | | Description | What the input is for, shown to whoever runs the workflow | Steps read an input's value with an `@` reference, like any other value. ## Giving inputs a value Whoever starts the run supplies the values: - **You**, when you run the workflow in Lodol - **Your code**, in the body of an API call: `{"inputs": {"client": "Acme", "month": "2026-05"}}`. See [Run a workflow](https://docs.lodol.com/docs/api/runs.md#run-a-workflow). - **Another workflow**, in its [Run a workflow](https://docs.lodol.com/docs/guides/sub-workflows.md) step - **An AI agent**, when the workflow is one of its [tools](https://docs.lodol.com/docs/guides/ai-agents.md#tools): the agent fills in the inputs itself Values are converted to each input's type where they can be: `"21"` becomes the number 21, `"yes"` becomes `true`, and `"a, b"` becomes a list. A value that can't be converted, a missing required input, or a name the workflow doesn't declare stops the run before it starts, with a message naming the input. ## Outputs A workflow can declare outputs: the values it hands back when it finishes. When another workflow runs it with a **Run a workflow** step, that step's result has one field per output, so later steps read them like any step's result. This is how you build a small workflow once, such as "look up a customer", and reuse it in many others. Your code gets them the same way: a run started through the API has them in its `outputs` once it succeeds. See [Get a run](https://docs.lodol.com/docs/api/runs.md#get-a-run). ## Next # Runs > A run is one pass through a workflow. Lodol records every run step by step, emails you when one fails, and counts each run once however many steps it takes. Web page: https://docs.lodol.com/docs/concepts/runs Each time a workflow starts, Lodol creates a **run**: one pass through the workflow from its trigger to its last step. In the Stripe example, every payment that comes in is one run. ## Every run is recorded A run's history shows every step it took, in order: what each step did, when, and how long it took. When a run fails, the history shows the step that stopped it. Open it from the workflow, or read it with the [API](https://docs.lodol.com/docs/api/runs.md#get-a-run). If a run fails, you get an email as soon as it does, and a notice in Lodol. Your [AI tool](https://docs.lodol.com/docs/build-with-ai.md) can read the same run, work out what went wrong and fix the workflow. ## Run statuses | Status | What it means | | --- | --- | | `queued` | Started, and about to begin its first step | | `running` | Working through its steps | | `success` | Reached the end of the workflow | | `failed` | A step failed and the run stopped there | | `stopped` | Someone stopped it before it finished | Stopping a run doesn't undo the steps it already completed: a message it sent stays sent. See [Handle failures](https://docs.lodol.com/docs/guides/failures.md) for keeping a run going when one step fails. ## Deleting runs A finished run can be deleted from its page, or several at once by ticking them in the **Runs** list. The runs it started go with it: the runs its trigger started, and the runs of the workflows it called. A run that is still going has to be stopped first. Deleting a run can't be undone, and needs the **Delete runs** permission in your [role](https://docs.lodol.com/docs/concepts/workflows.md#who-can-do-what). It doesn't give back the run it counted as. Lodol also deletes finished runs on its own once they're older than your plan keeps them. ## Some steps wait **Request approval** pauses a run until a person answers. The run waits without using any resources, then carries on from that step with the answer. **Wait** pauses a run for a set time, or until a date and time. ## What counts as a run Your plan includes a number of runs each month. A run counts once, however many steps it takes, whether you started it or a trigger, a schedule or the API did. These don't count: - a workflow that is turned on and waiting for its trigger - a workflow that another workflow runs with a **Run a workflow** step Runs reset on the 1st of each month (UTC). The workspace owner gets an email when 80% of the month's runs are used. If they run out, new runs pause until the 1st, unless you add more runs or turn on extra runs in **Billing**. See [pricing](https://www.lodol.com/pricing) for each plan's runs. ## Next # Connections and secrets > Workflows use your app accounts through connections, and your own API keys as secrets. Both are encrypted, used by name, and never shown in a workflow or to your AI. Web page: https://docs.lodol.com/docs/concepts/connections A workflow never holds a password or an API key. It names the account or key it needs, and Lodol supplies it when a step runs. That keeps credentials out of workflows, out of your AI tool's chat, and out of anything you share. ## Connections A **connection** is your workspace's access to one of your apps, such as your Google account or your Slack workspace. App actions and triggers run through it. - **You connect an app once.** When a workflow first uses an app your workspace hasn't connected, its step shows a **Connect** button. Some apps ask you to sign in; others ask for an API key from the app's own settings, such as a Stripe key from the Stripe dashboard. - **Connections are shared.** Members of the workspace use its connections in their workflows, so an app is connected once for everyone. - **AI models use your own key.** The [AI agent](https://docs.lodol.com/docs/guides/ai-agents.md) step runs on your connection to an AI provider such as Anthropic, OpenAI or Google Gemini, so the provider bills your account directly. Connect one under **Integrations** in Lodol. Your AI tool can't connect apps for you: connecting always happens in Lodol, by someone in your workspace. ## Secrets A **secret** is a key or password you add yourself, for a service that has no app in Lodol. A [Call an API](https://docs.lodol.com/docs/guides/http-and-code.md#call-an-api) step uses it by name, usually in an `Authorization` header. - Secrets are encrypted at rest, and credentials are encrypted in transit. - Workflows and AI tools see only a secret's name, never its value. - A secret can belong to the whole workspace or to one workflow. Deleting a workflow keeps its secrets: they're detached from it, not deleted. A **Call an API** step sends only the secrets you give it. It can't use an app's connection, so calling Google's or Slack's API directly with it won't be signed in. For apps Lodol has, use their actions; keep **Call an API** for services Lodol doesn't connect to. ## Values Values are named settings your workspace keeps that aren't secret, such as a base URL several workflows share. Steps read them as `@values.NAME`. Workflows can also save and update values as they run, to [remember things between runs](https://docs.lodol.com/docs/guides/stored-values.md). ## Next # Make decisions > Use If to run steps only when something is true, Otherwise for when it isn't, and a chain of Ifs for three or more cases. Web page: https://docs.lodol.com/docs/guides/conditions An **If** step runs the steps inside it only when its condition is true. An **Otherwise** step after them holds the steps to run when it isn't. An **End if** step closes the section, and the workflow carries on with whatever comes after it either way. ## Write the condition The If step's **Only continue if** field takes the condition. Write it the way you'd say it, as a test of values from earlier steps: - `@payment.amount` is at least `100000` - `@email.subject` contains `invoice` - `@row.Status` is `""` (the cell is empty) - `@today` is a public holiday Combine tests with **and** and **or**. A range needs both ends: "days left is at least 0 **and** at most 30", or past dates slip in. ### Or describe it in plain words When a decision doesn't fit a rule, such as "this email is a sales lead", describe the condition in your own words and let the workflow judge it. Only that condition uses AI; every other step still runs the same way each time. If later steps need more than a yes or no, such as a category and a reason, use an [AI agent](https://docs.lodol.com/docs/guides/ai-agents.md#get-fields-back) that returns those fields, and test them with ordinary conditions. ## Three or more cases To choose between several cases, put the next **If** directly inside the **Otherwise**. Lodol treats the chain as one decision with a path for each case, and the last **Otherwise** catches anything left over: Keep the Otherwise holding nothing but the next If. Steps that every case shares go after the whole chain. ## Avoid the usual surprises - **Text is compared exactly.** "Paid" is not "paid". Compare text that people type through **Format text** set to lowercase. - **Compare money in the unit you mean.** Stripe amounts are in cents. Divide by 100 in a step before the If, and the condition reads "at least 1000". - **Compare dates as days.** To check "within 30 days", use **Difference between dates** and compare the number, not the dates themselves. - **Approvals give you a yes or no.** After a [Request approval](https://docs.lodol.com/docs/guides/approvals.md) step, test `@approval.approved` rather than matching words someone typed. ## Reference Every field of the If and Otherwise steps is on the [Decisions](https://docs.lodol.com/docs/integrations/built-in/conditions.md) reference page. # Repeat steps > Use Repeat to run steps once for each item in a list, a set number of times, or while something is true, and avoid the mistakes that make loops quietly wrong. Web page: https://docs.lodol.com/docs/guides/loops A **Repeat** step runs the steps inside it more than once. An **End repeat** step closes the section. There are three kinds: | Step | Runs its steps | Use it when | | --- | --- | --- | | **Repeat for each item** | Once for each item in a **List** | You have records to handle one at a time: rows, contacts, files | | **Repeat N times** | The **Number of times** you set | You know exactly how many passes you need | | **Repeat while** | While **Keep repeating while** holds | You don't know in advance, such as fetching pages until there are no more | ## Repeat for each item Inside the loop, the current item has a name, such as `row`, and steps read it like any value: `@row.Email`. Two rules keep a loop right: - **Loop over the list, not the whole result.** An action that returns a list keeps it in a field: loop over `@sheet.rows`, not `@sheet`. - **Every step inside should use the item.** A step that never mentions `@row` does the same thing on every pass. If it fetches something, fetch it once before the loop instead. ## Often you don't need a loop Built-in list steps handle a whole list at once, and are shorter and faster than a loop: | To | Use | | --- | --- | | Keep only the items that match a condition | **Filter list** (the current item is `@item` in its condition) | | Add up, average or find the largest of a field | **Get one field from every item**, then **Calculate total**, **Calculate average** or **Find maximum value** | | Find whether a list includes something | **Check if list includes item** | | Take the first few items | **Take part of list** | And when an app can search for exactly the records you want, such as Zendesk tickets older than a week, use its search action instead of listing everything and testing each one in a loop. It can't miss records that fall past the first page. ## One message per person To email each person about their own items, don't send one email per item. Group the items first: ### Group the records Use **Group items by a field** on the list, with **Field to group by** set to the person, such as `owner` or `assignee.displayName`. Each group has a `key` (the shared value), its `items`, and a `count`. ### Repeat for each group Inside the loop, `@group.key` is the person and `@group.count` how many items they have. ### Send one message per group List the items with **Get one field from every item** on `@group.items`, turn the list into lines with **Join list into text**, and send that in one message. Records with no owner form one group with an empty key. Leave them out in the search itself, or skip the group whose key is empty. ## One message with a line per group To send a single summary with a line for each group, such as revenue by plan, start with an empty list before the loop. In each pass, work out and format the figure, then use **Add item to end** to add one line to the list. After the loop, **Join list into text** with a line break, and use the result in the message. ## Keep going when one item fails By default, a step that fails stops the run, so one bad record ends the loop for everyone after it. To skip the failure and carry on with the next item, put the steps that can fail in a try section. See [Handle failures](https://docs.lodol.com/docs/guides/failures.md#let-steps-fail-without-stopping-the-run). ## Reference Every field of the Repeat steps is on the [Repeat](https://docs.lodol.com/docs/integrations/built-in/loops.md) reference page, and every list step on [Lists](https://docs.lodol.com/docs/integrations/built-in/lists.md). # Ask for approval > Pause a run until someone approves or rejects it, or answers a question in writing, then carry on with their answer. People outside your workspace can answer too. Web page: https://docs.lodol.com/docs/guides/approvals A **Request approval** step pauses the run and asks people to decide. They get an email and a notice in Lodol, each with a link, and anyone outside your workspace can answer from the link without an account. The run waits without using any resources, then carries on with the answer. It asks for one of two things, set in **What you need back**: - **Approve or reject** (the default): for a sign-off or a control, such as before paying a bill or posting in public. - **A written reply**: people answer a question only a person can, such as which PO number an invoice goes against. ## Ask for an approval ### Say what needs deciding **What to ask** is the one line people see first, such as `Pay @bill.vendor_name $@bill.total for bill @bill.doc_number`. Put everything else they need in **Details**: amounts, line items and links to the documents. Links are clickable in the email and in Lodol. ### Choose who decides Put their email addresses in **Who to ask**, separated by commas. People in your workspace answer in Lodol; anyone else gets a secure link by email. **Who can answer** sets whether only those people can decide, those people plus the workspace's owner and admins, or anyone in the workspace who can run workflows. ### Act on the decision Add an **If** step after it that tests `@approval.approved`, with the approved steps inside and the rejected steps under **Otherwise**. With no one listed, anyone in the workspace who can run workflows can approve. That's fine for a quick check, but not for a control. For tiered approvals especially, name the people at each level. ## Ask for a written reply Set **What you need back** to a written reply and write the question as **What to ask**: `Which PO number should the @bill.vendor_name bill go against?`. Later steps read the answer as `@answer.reply`. Use a reply only when a person really has to supply information. For a yes or no, use an approval: it gives you `approved` as true or false, not words to match. ## When nobody answers | Setting | What it does | | --- | --- | | **Time limit (hours)** | How long to wait. 72 hours if left empty, at most 720 (30 days). Fractions work: `0.5` is half an hour. | | **If nobody answers in time** | Left empty, an approval is rejected and a written reply stops the run with an error. An approval can instead **Treat it as approved** or **Stop with an error**. A reply can **Continue without a reply**, which gives the next steps an empty reply with `timed_out` set. | | **Remind every (hours)** | Reminds the people asked this often while the request waits, up to 10 reminders. | | **Escalate after (hours)** | If nobody has answered after this long, also asks the people in **Escalate to**, or the workspace's owner and admins if that's empty. It must be shorter than the time limit. | ## Keep approvals honest Two settings keep the person who asked from approving their own request: - **Require a different approver** stops the person the run runs as from approving it: whoever started the run, or the workflow's owner for a scheduled or triggered run. - **Prepared by** names whoever prepared the item, such as the person who entered the bill. They can't approve it. These apply to approvals, not to written replies. ## Approve in steps - **Several levels:** chain Request approval steps, each followed by its If, so a second person only sees what the first approved. Use different people at each level, or the second level controls nothing. - **Route by amount:** put Request approval steps inside If branches, such as a manager for bills under $5,000 and the finance lead above. - **Approve small amounts automatically:** start with an `approved` value set to true, ask for approval only inside an If for large amounts, and act on the result once after the If, so both paths share the same steps. ## Post the request in Slack **Also post to Slack channel** posts the request to a channel, with a link to answer, using your workspace's Slack connection. The Lodol app must be a member of the channel. ## What the step returns | Output | What it holds | | --- | --- | | `approved` | `true` or `false` for an approval | | `decision` | The decision, in words | | `reply` | The written answer, for a reply | | `comment` | Any comment left with a decision | | `decided_by` | Who answered | | `decided_at` | When they answered | | `timed_out` | Whether the time limit ran out | | `approval_id` | The request's ID | Every field of the step is on the [Approvals](https://docs.lodol.com/docs/integrations/built-in/review.md) reference page. # Use an AI agent > Give an AI a task, files to read and tools to use, and get back an answer or the exact fields you ask for. Use it where the work has no fixed recipe. Web page: https://docs.lodol.com/docs/guides/ai-agents A **Run an AI agent** step hands a task to an AI model. The agent works out which of its tools to use and when, then hands back an answer, or the exact fields you asked for, for the steps after it. ## When to use an agent Use an agent where the steps can't be known in advance: - **Reading documents:** pull the vendor, total and due date out of an invoice, a CV or a screenshot. - **Judging:** decide whether an email is a sales lead, or which of several pages answers a question. - **Researching:** work out who the business behind a bank-feed line is, deciding what to look at next from each search result. - **Conversations:** answer messages in a chat, remembering earlier turns. When you already know the steps, such as search, then read, then write, build them as ordinary steps instead. An agent costs model usage on every run and can take a different path each time; ordinary steps run the same way every time at no extra cost. ## Set up the step ### Choose a model **Model** sets which model runs the agent, from Anthropic, OpenAI, Google Gemini or another AI provider you've connected under **Integrations**. It runs on your own API key, so the provider bills your account; Lodol supplies the agent, not the model. **Fallback model** takes over if the first one fails to answer, for example during an outage. ### Write the task and the instructions **Task** is what to do on this run, written the way you'd brief a colleague and mentioning the values it needs: `Find out who the vendor behind this bank line is: @line.description`. **Instructions** are the standing rules for every run: whose books these are, what counts as found, what to leave out. ### Give it what it needs Add **Files** for it to read, **Tools** for it to use, and **Fields to return** for the answer you want back. Each is described below. ## Get fields back Without **Fields to return**, the agent hands back its answer as text. With them, it must fill in each field before it finishes, and later steps read them by name, such as `@triage.output.category`. Describe each field, including the answers you'll accept: ```txt title="Fields to return" category Exactly "urgent", "question" or "other" reason One sentence on why ``` This is how to branch on an AI's judgment: return a field, then test it with an [If](https://docs.lodol.com/docs/guides/conditions.md). Don't ask a model for a magic word in free text and then match the prose. ## Read files **Files** takes PDFs or images: a file you upload, a link, or a file an earlier step downloaded, such as Gmail's **Download attachments**. Up to 10 files, 20 MB in total. Reading files needs a model from Anthropic, OpenAI or Google Gemini. To pull fields out of a document, give the agent the file and **Fields to return**, and no tools. ## Tools **Tools** are what the agent may use while it works: - **Actions from your apps**, such as a web search or reading a page. They run with your workspace's connections, so connect the app first. - **Your own workflows**: any published workflow that doesn't start from a trigger. The agent fills in its inputs and gets its outputs back, which is how to give it a fixed procedure, or another agent. Keep the list short, two to four tools. For any input you don't want the agent to choose, such as which spreadsheet to write to, fix its value: it's filled in on every call and hidden from the agent. **Turn limit** caps how many turns the agent takes before it must finish: 10 unless you change it, at most 30. Each turn is one model call and may use several tools. ## Remember a conversation For a workflow that answers messages, set **Conversation key** to something that identifies the conversation, such as the Slack thread or the sender's email address. Runs with the same key see what was asked and answered before; **Runs to remember** sets how many, 10 unless you change it, at most 50. Leave the key empty for one-off tasks, so each run starts fresh. ## What the step returns | Output | What it holds | | --- | --- | | `output` | The fields you asked for, filled in; or the agent's final text when you asked for none | | `text` | The agent's final message | | `steps` | How many turns it took | | `tool_calls` | Each tool it used, with its input, whether it succeeded, and the start of what came back | | `model` | The model that finished the run | | `usage` | The tokens the run used | Every field of the step is on the [AI Agent](https://docs.lodol.com/docs/integrations/built-in/agent.md) reference page. # Remember values between runs > Stored values let a workflow pick up where it left off, keep a count, or share a value with other workflows, from one run to the next. Web page: https://docs.lodol.com/docs/guides/stored-values Each run starts fresh: a step's result lasts only until the run ends. To keep something for the next run, such as the last record a workflow processed, save it as a **stored value**. ## The four steps | Step | What it does | | --- | --- | | **Set a stored value** | Saves a **Value** under a **Name**, replacing what was there | | **Get a stored value** | Reads it back, or returns the **Default value** when nothing is stored yet | | **Increment a stored value** | Adds an **Amount** (1 unless you change it) to a counter and returns the new total | | **Delete a stored value** | Removes it; deleting something that was never set does nothing | Each value has a **Scope**: the whole workspace, shared by every workflow, or just this workflow. ## Pick up where it left off A scheduled workflow that handles new records can remember how far it got: **Get a stored value** returns `found`, which is false the first time, and `value`. Give it a **Default value** so the first run knows where to start. ## Count things safely **Increment a stored value** is safe to run from many runs at once: every increment is counted, with none lost. The counter starts at zero the first time. Its value must be a number; if it holds something else, the step fails and nothing is added. ## Names and fields Names use letters, digits and underscores, and each part can't start with a digit, such as `last_seen` or `invoice_count`. To work with one field inside a stored object, add a dotted path: `customer.email`. Setting a dotted path fills in any missing objects along the way. ## Letting workflows change a value A workflow can always read a stored value. To set, increment or delete one that someone created in the workspace, that value needs **Let workflows edit this value** turned on. A value a workflow creates itself can always be edited by workflows. ## Reference Every field of these steps is on the [Stored Data Values](https://docs.lodol.com/docs/integrations/built-in/variables.md) reference page. # Run a workflow from another > Build a procedure once, such as "look up a customer", and use it as a step in any workflow. It takes inputs, hands back outputs, and doesn't count as an extra run. Web page: https://docs.lodol.com/docs/guides/sub-workflows A **Run a workflow** step runs another workflow in your workspace and waits for it to finish. Values go in through the inputs it declares, and its outputs come back as the step's result. Use it to build a procedure once and reuse it in many workflows, so a fix in one place fixes it everywhere. ## What a workflow needs to be run this way - It's **published** in the same workspace. - It **doesn't start from a trigger or a schedule**: it runs on demand. - It declares the [inputs](https://docs.lodol.com/docs/concepts/inputs-and-outputs.md) it needs and the outputs it hands back. ## Set up the step Choose the workflow in **Workflow**, then give a value for each of its **Inputs**. Each value is checked against the type the workflow declared for it. Later steps read its outputs from the step's result, one field per output. The step also keeps the workflow's name, so it still reads clearly if the workflow is renamed later. ## How it runs - The workflow you call runs as part of this run, and gets its own entry in the run history. - It doesn't count as a separate run on your plan. - A workflow can't run itself, or any workflow already in the chain of calls that led to it. ## Give an AI agent a procedure A workflow that can be run this way can also be one of an [AI agent's tools](https://docs.lodol.com/docs/guides/ai-agents.md#tools). The agent fills in the inputs and reads the outputs, so it follows your procedure exactly instead of improvising one. ## Reference Every field of the step is on the [Workflows](https://docs.lodol.com/docs/integrations/built-in/workflows.md) reference page. # Handle failures > When a step fails, the run stops and Lodol tells you. Here's how to find out why, fix it, and keep a run going when a failure shouldn't end it. Web page: https://docs.lodol.com/docs/guides/failures Steps that reach outside Lodol can fail for reasons outside your control: an app is down, a record doesn't exist, a key has expired. By default a failed step stops the run, so nothing after it runs on bad data. ## When a run fails - The run stops at the failed step, with the status `failed`. - You get an email as soon as it happens, and a notice in Lodol. - The run's history shows every step it took, with timings, and the step that stopped it. To fix it, open the run, see which step failed and why, and change the workflow. Your [AI tool](https://docs.lodol.com/docs/build-with-ai.md) can do the same: ask it to look at the failed run and fix the workflow. Every change is a new [version](https://docs.lodol.com/docs/concepts/workflows.md#every-change-is-a-version), so you can roll back if a fix makes things worse. Stopping a run, or a run failing, doesn't undo the steps that already finished. ## Let steps fail without stopping the run Some failures shouldn't end the run. In a loop over a hundred records, one bad record shouldn't stop the other ninety-nine. Put the steps that can fail in a **try section**: - If every step in it succeeds, the run carries on as normal. - If one fails, the rest of the section is skipped and the steps you set for that case run instead. They can read the failure's `message`, and its `type`: `action`, `http`, `timeout`, `variable` or `error`. - With no steps for that case, the failure is skipped and the run carries on with whatever comes after the section. Use it around a step whose failure you can handle: notify someone, fall back to another source, or record the problem and move on. If nothing in a section can fail, it doesn't need one. ### Find or create A try section is also the clean way to "find a record, or create it if it doesn't exist": try the lookup, and create the record in the steps that run when the lookup fails. Here, a failed lookup simply means "not found". ## Choose what a step does when it fails Some steps let you decide how a failure is handled without a try section: | Step | Setting | Options | | --- | --- | --- | | [Call an API](https://docs.lodol.com/docs/guides/http-and-code.md#call-an-api) | **If the call fails** | Fail the run (the default), or continue so later steps can check the status, which suits a 404 that just means "not found" | | [Request approval](https://docs.lodol.com/docs/guides/approvals.md#when-nobody-answers) | **If nobody answers in time** | Reject, approve, fail the run, or continue | ## Avoid failures before they happen - **Check lookups that match by name.** Matching an issue's assignee to a Slack user by name is a guess. Test whether the lookup found someone before messaging them. - **Fill in every place the workflow points at.** A step whose sheet, channel or record hasn't been chosen yet fails on the first run. Set them all before you turn the workflow on. - **Skip empty lists.** A step that summarizes a week of tickets should only run when there are tickets: put it inside an If that checks the count. # Call an API or run code > Reach any service with a REST or GraphQL API, or compute something no built-in step covers. Use them when nothing else fits. Web page: https://docs.lodol.com/docs/guides/http-and-code Two built-in steps can do almost anything: **Call an API** reaches any web API, and **Run Python code** runs a short script. Every other step shows its app and labeled fields, which anyone on your team can read and change. These two show a URL and headers, or a block of code. Reach for them when no app action or built-in step does the job. ## Call an API **Call an API** calls a REST or GraphQL endpoint that Lodol has no app for, and hands the response to later steps. | Field | What it's for | | --- | --- | | **URL** | The full address, starting with `https://`. It can include workspace values, such as `@values.BASE_URL` followed by a path. | | **Method** | `GET` unless you choose another | | **Headers** | Such as an `Authorization` header, using a [secret](https://docs.lodol.com/docs/concepts/connections.md#secrets) for the token | | **Query parameters** | Added to the end of the URL | | **Body** | What to send, as JSON or a value from an earlier step. GraphQL queries go here too. | | **Body format** | `json` (the default), `form`, `text` or `none` | | **Response format** | `auto` (the default) follows the response's content type, so JSON becomes an object and a download becomes a file | | **If the call fails** | Fail the run (the default), or continue so later steps can check the status | The step returns the response's `status`, `ok` (true for a success status), `headers`, `body` and `url`. A Call an API step can't use an app's connection; it only has the secrets you give it. Calling Google's or Slack's API with it goes out without your account. For any app in the [directory](https://docs.lodol.com/docs/integrations.md), use its actions. ## Run Python code **Run Python code** runs a short script, for the rare reshaping or calculation no built-in step covers. - **Inputs** maps names to values from earlier steps; the script reads them by those names. - The script's result is whatever it assigns to a variable named `output`. - **Output type** checks the result before later steps use it: `auto` (the default), `text`, `number`, `boolean`, `list` or `object`. ```python title="Python code: check an IBAN" # Inputs: iban (text). Output type: boolean. iban = iban.replace(" ", "").upper() rearranged = iban[4:] + iban[:4] digits = "".join(str(int(char, 36)) for char in rearranged) output = int(digits) % 97 == 1 ``` The script runs in a secure sandbox with limits on time and memory. It has no internet or file access: use Call an API or an app's action to reach a service, and [Read a file](https://docs.lodol.com/docs/integrations/built-in/files.md#read-a-file) to get what's inside a file. Before writing code, check the built-in steps: most text, list, number and date work, such as grouping records by a field, totalling a column or formatting a date, already has a step. ## Reference Every field is on the [HTTP](https://docs.lodol.com/docs/integrations/built-in/http.md) and [Code](https://docs.lodol.com/docs/integrations/built-in/code.md) reference pages. # Developer API > Start workflows from your own code, pass them inputs, and read what each run did. A small REST API with JSON bodies and API-key authentication. Web page: https://docs.lodol.com/docs/api The developer API lets your own systems use the workflows you've built in Lodol: list them, start runs with the inputs they need, follow each run to its result, and stop or delete a run. Workflows themselves are built in Lodol, or by your [AI tool](https://docs.lodol.com/docs/build-with-ai.md). ## Base URL Every endpoint is under one base URL: ```txt https://api-prod.lodol.com/api/v1 ``` ## Endpoints | Endpoint | What it does | Scope | | --- | --- | --- | | [`GET /workflows`](https://docs.lodol.com/docs/api/workflows.md#list-workflows) | List the workspace's workflows | `workflows:read` | | [`GET /workflows/:id`](https://docs.lodol.com/docs/api/workflows.md#get-a-workflow) | Get one workflow, with its steps | `workflows:read` | | [`DELETE /workflows/:id`](https://docs.lodol.com/docs/api/workflows.md#delete-a-workflow) | Delete a workflow for good | `workflows:delete` | | [`POST /workflows/:id/run-async`](https://docs.lodol.com/docs/api/runs.md#run-a-workflow) | Start a run | `workflows:execute` | | [`GET /executions`](https://docs.lodol.com/docs/api/runs.md#list-runs) | List runs | `executions:read` | | [`GET /executions/:id`](https://docs.lodol.com/docs/api/runs.md#get-a-run) | Get a run's status and results | `executions:read` | | [`POST /executions/:id/stop`](https://docs.lodol.com/docs/api/runs.md#stop-a-run) | Stop a run | `workflows:execute` | | [`DELETE /executions/:id`](https://docs.lodol.com/docs/api/runs.md#delete-a-run) | Delete a finished run for good | `executions:delete` | The API calls a run an **execution**: run IDs are `execution_id`, and their endpoints are under `/executions`. ## Requests and responses - **Authentication.** Send your API key as a bearer token: `Authorization: Bearer sk_live_…`. See [Authentication](https://docs.lodol.com/docs/api/authentication.md). - **JSON.** Request bodies are JSON, sent with `Content-Type: application/json`. Every response is JSON. - **Timestamps** are ISO 8601 with an offset, such as `2026-03-17T08:00:00+00:00`. - **Errors** have a JSON body with an `error` message and a standard HTTP status. See [Errors and retries](https://docs.lodol.com/docs/api/errors.md). - **Retries.** Send an `Idempotency-Key` with each run you start, so retrying a request never starts the same run twice. See [Idempotency](https://docs.lodol.com/docs/api/errors.md#idempotency). - **Limits.** Requests are rate-limited per workspace, by plan. See [Limits](https://docs.lodol.com/docs/api/limits.md). ## Paging through lists [List runs](https://docs.lodol.com/docs/api/runs.md#list-runs) returns results a page at a time. Pass `limit` (1 to 100, default 20) for the page size, and `after` with the last ID of the page you have to get the next one. ```bash # First page curl "https://api-prod.lodol.com/api/v1/executions?limit=20" \ -H "Authorization: Bearer $LODOL_API_KEY" # Next page: after = the last execution_id of the previous page curl "https://api-prod.lodol.com/api/v1/executions?limit=20&after=683b2c3d4e5f6a7b8c9d0e1f" \ -H "Authorization: Bearer $LODOL_API_KEY" ``` ## Version This is version 1 of the API, named in the path as `/api/v1`. # Quickstart > Create an API key, start a workflow run from your code, and read its result. About five minutes. Web page: https://docs.lodol.com/docs/api/quickstart You need a Lodol workspace with at least one workflow, and permission to create API keys there: workspace owners and admins can. ### Create an API key In Lodol, go to **Settings › API Keys** and create a key with the `workflows:read`, `workflows:execute` and `executions:read` scopes. Copy it: the full key is shown only once. Keep it in an environment variable rather than in your code: ```bash export LODOL_API_KEY="sk_live_..." ``` ### Find your workflow List your workflows to get the `id` of the one to run. ```bash tab="cURL" tab-group="lang" curl https://api-prod.lodol.com/api/v1/workflows \ -H "Authorization: Bearer $LODOL_API_KEY" ``` ```js tab="JavaScript" const response = await fetch('https://api-prod.lodol.com/api/v1/workflows', { headers: { Authorization: `Bearer ${process.env.LODOL_API_KEY}` }, }); const { workflows } = await response.json(); console.log(workflows.map((w) => `${w.id} ${w.name}`).join('\n')); ``` ```python tab="Python" import os import requests response = requests.get( "https://api-prod.lodol.com/api/v1/workflows", headers={"Authorization": f"Bearer {os.environ['LODOL_API_KEY']}"}, ) for workflow in response.json()["workflows"]: print(workflow["id"], workflow["name"]) ``` Each workflow lists the `inputs` it declares, if any: ```json { "workflows": [ { "id": "665f1a2b3c4d5e6f7a8b9c0d", "name": "Monthly Client Report", "description": "Builds a client's monthly report and emails it to them", "inputs": [ { "name": "client", "type": "text", "required": true, "default": null, "description": "The client to report on" } ] } ] } ``` ### Start a run Start the workflow with its inputs. The `Idempotency-Key` makes the request safe to retry: sending it again returns the same run instead of starting another. ```bash tab="cURL" tab-group="lang" curl -X POST https://api-prod.lodol.com/api/v1/workflows/665f1a2b3c4d5e6f7a8b9c0d/run-async \ -H "Authorization: Bearer $LODOL_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: acme-report-2026-05" \ -d '{"inputs": {"client": "Acme"}}' ``` ```js tab="JavaScript" const response = await fetch( 'https://api-prod.lodol.com/api/v1/workflows/665f1a2b3c4d5e6f7a8b9c0d/run-async', { method: 'POST', headers: { Authorization: `Bearer ${process.env.LODOL_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': 'acme-report-2026-05', }, body: JSON.stringify({ inputs: { client: 'Acme' } }), }, ); const run = await response.json(); console.log(run.execution_id, run.status); ``` ```python tab="Python" response = requests.post( "https://api-prod.lodol.com/api/v1/workflows/665f1a2b3c4d5e6f7a8b9c0d/run-async", headers={ "Authorization": f"Bearer {os.environ['LODOL_API_KEY']}", "Idempotency-Key": "acme-report-2026-05", }, json={"inputs": {"client": "Acme"}}, ) run = response.json() print(run["execution_id"], run["status"]) ``` The API answers straight away with `202 Accepted` and the run's ID, while the run carries on in the background: ```json { "execution_id": "683b2c3d4e5f6a7b8c9d0e1f", "workflow_id": "665f1a2b3c4d5e6f7a8b9c0d", "version_id": "6683e1c4b2a9d7f0e5c3a1b2", "status": "queued", "is_trigger_listener": false, "created_at": "2026-04-14T10:30:00+00:00" } ``` A workflow with no required inputs can be started without a body. ### Wait for the result Check the run until it finishes: `success`, `failed` or `stopped`. A run that succeeded has what the workflow handed back in `outputs`, one field per output it declares; one that failed says why in `error`. ```bash tab="cURL" tab-group="lang" curl https://api-prod.lodol.com/api/v1/executions/683b2c3d4e5f6a7b8c9d0e1f \ -H "Authorization: Bearer $LODOL_API_KEY" ``` ```js tab="JavaScript" const headers = { Authorization: `Bearer ${process.env.LODOL_API_KEY}` }; const url = `https://api-prod.lodol.com/api/v1/executions/${run.execution_id}`; const finished = ['success', 'failed', 'stopped']; let result; do { await new Promise((resolve) => setTimeout(resolve, 5000)); result = await (await fetch(url, { headers })).json(); } while (!finished.includes(result.status)); console.log(result.status, result.outputs, result.error); ``` ```python tab="Python" import time headers = {"Authorization": f"Bearer {os.environ['LODOL_API_KEY']}"} url = f"https://api-prod.lodol.com/api/v1/executions/{run['execution_id']}" while True: time.sleep(5) result = requests.get(url, headers=headers).json() if result["status"] in ("success", "failed", "stopped"): break print(result["status"], result["outputs"], result["error"]) ``` ```json { "execution_id": "683b2c3d4e5f6a7b8c9d0e1f", "workflow_id": "665f1a2b3c4d5e6f7a8b9c0d", "version_id": "6683e1c4b2a9d7f0e5c3a1b2", "workflow_name": "Monthly Client Report", "status": "success", "is_trigger_listener": false, "created_at": "2026-04-14T10:30:00+00:00", "started_at": "2026-04-14T10:30:00+00:00", "completed_at": "2026-04-14T10:30:45+00:00", "error": null, "outputs": { "total": 1250, "summary": "A good month for Acme" } } ``` Every check counts toward your [rate limit](https://docs.lodol.com/docs/api/limits.md), so poll every few seconds rather than in a tight loop. To see what each step produced along the way, add `include_step_results=true`. ## Next steps # Authentication > Every request carries an API key. A key belongs to one workspace, its scopes say what it may do, and it can never do more than the member who created it. Web page: https://docs.lodol.com/docs/api/authentication Send your API key as a bearer token in the `Authorization` header of every request: ```bash curl https://api-prod.lodol.com/api/v1/workflows \ -H "Authorization: Bearer $LODOL_API_KEY" ``` A request without a valid key gets `401 Unauthorized`. ## Create a key Create and revoke keys in Lodol under **Settings › API Keys**. Only the workspace's owners and admins can. - **A key belongs to one workspace.** Every request it makes works with that workspace's workflows and runs, and its runs count toward that workspace's plan. - **You see the full key once,** when you create it. Lodol keeps only its first characters and a hash, so a lost key can't be recovered: revoke it and create another. - **Keys look like** `sk_live_` followed by a random string with 256 bits of entropy. - **Keys can expire.** Set an expiry when you create a key, and create a new one before it lapses. The number of keys a workspace can have depends on its plan: | Plan | API keys | | --- | --- | | Free | 1 | | Builder | 10 | | Pro | 40 | | Enterprise | Custom | ## Scopes Each key has one or more scopes, which decide the endpoints it can call: | Scope | Allows | | --- | --- | | `workflows:read` | Listing workflows and reading a workflow | | `workflows:execute` | Starting runs and stopping them | | `executions:read` | Listing runs and reading a run | | `workflows:delete` | Deleting workflows. Not included unless you tick **Delete workflows** when you create the key. | | `executions:delete` | Deleting runs. Not included unless you tick **Delete runs** when you create the key. | Scopes add up: a key with `workflows:read` and `workflows:execute` can list and run workflows, but can't read runs without `executions:read`. Give each key only the scopes its job needs. ## A key acts for the member who created it A request has to be allowed by both the key's scopes and the [role](https://docs.lodol.com/docs/concepts/workflows.md#who-can-do-what) of the member who created the key: - Running or stopping a workflow also needs the creator's role to include **Run workflows**. - Deleting a workflow also needs the creator's role to include **Delete workflows**. - Deleting a run also needs the creator's role to include **Delete runs**. If the creator's role loses a permission, their keys lose it too. If the creator leaves the workspace, their keys stop working on every endpoint, so create replacement keys before someone leaves. ## Keep keys safe - Never put a key in code that runs in a browser or a mobile app, or commit it to a repository. - Keep keys in environment variables or a secrets manager. - Use one key per integration, with the fewest scopes it needs, so you can revoke one without breaking the others. - Revoke keys you no longer use. # Errors and retries > The error codes the API returns and what to do about each, the request ID to quote to support, which requests are safe to retry, and how idempotency keys stop a retry from starting a run twice. Web page: https://docs.lodol.com/docs/api/errors The API uses standard HTTP status codes. Every error has a JSON body with a message saying what went wrong, and a code naming the problem: ```json { "error": "Execution not found", "error_code": "not_found" } ``` Decide what to do from `error_code`. It names the problem and never changes, while the wording of `error` is for people and may be improved. The [error codes](#error-codes) below list every one. ## Request IDs Every response, error or not, carries an `X-Request-Id` header naming the request: ```http X-Request-Id: req_9808efd9bf000072c581da45b425d3c0 ``` When something goes wrong and you contact Lodol's support, quote it: it finds the request in Lodol's records. Log it with any error you keep. ## Status codes | Status | Meaning | What to do | | --- | --- | --- | | `200` | Success | | | `202` | Accepted: the run has started | Follow it with [Get a run](https://docs.lodol.com/docs/api/runs.md#get-a-run) | | `400` | The request isn't valid, such as an input that doesn't match the workflow | Fix the request; the message names the problem. Don't retry as is. | | `401` | The API key is missing or invalid | Check the `Authorization` header and the key | | `402` | Lodol Local only: the installation needs a licence | Add the licence in **Settings** | | `403` | The key lacks a scope, its creator's role doesn't allow the action, or the plan doesn't include it | Use a key with the right scope, or change the creator's role | | `404` | The workflow or run doesn't exist, or was deleted | Check the ID | | `409` | A request with the same `Idempotency-Key` is still being processed, or a run to delete hasn't finished | Depends on the `error_code`: see [Error codes](#error-codes) | | `422` | The `Idempotency-Key` was already used for a different request | Use a new key for a new request | | `429` | Too many requests, too many runs in progress at once, or the month's runs are used up | Depends on the `error_code`: see [Error codes](#error-codes) | | `500` | Something went wrong on Lodol's side | Retry with backoff | ## Error codes These name a problem precisely, so your code can handle each its own way: | Code | Status | Meaning | What to do | | --- | --- | --- | --- | | `missing_api_key` | `401` | No `Authorization: Bearer …` header | Send the key | | `invalid_api_key` | `401` | The key is malformed, revoked or expired | Check the key, or make a new one | | `missing_scope` | `403` | The key was made without a scope this endpoint needs | Use a key with the scope | | `permission_denied` | `403` | The member who made the key isn't allowed to do this | Change their role, or use another member's key | | `api_key_owner_inactive` | `403` | The member who made the key has left the workspace, or has no role in it | Make a new key | | `item_view_only` | `403` | The key's member can see this workflow but not run or change it | Ask for edit access to it | | `workspace_ip_not_allowed` | `403` | The workspace only accepts requests from certain networks | Send the request from an allowed network | | `invalid_inputs` | `400` | The run's inputs don't match what the workflow declares, or it takes none | Fix the inputs; the message names the one at fault | | `workflow_not_runnable` | `400` | The workflow is a draft, or isn't finished | Publish or fix it in Lodol | | `run_already_finished` | `400` | [Stop a run](https://docs.lodol.com/docs/api/runs.md#stop-a-run) on a run that has already finished | Nothing: the run is over. [Get it](https://docs.lodol.com/docs/api/runs.md#get-a-run) to see how it ended | | `run_not_finished` | `409` | [Delete a run](https://docs.lodol.com/docs/api/runs.md#delete-a-run) on a run that, or a run it started, hasn't finished | [Stop it](https://docs.lodol.com/docs/api/runs.md#stop-a-run), or wait for it to finish, then delete it | | `request_in_progress` | `409` | A request with this `Idempotency-Key` is still being worked on | Retry after the `Retry-After` header's seconds | | `idempotency_key_reused` | `422` | This `Idempotency-Key` was already used for a different request | Use a new key for a new request | | `rate_limited` | `429` | Too many requests in the last minute | Retry after the `Retry-After` header's seconds | | `concurrency_limit` | `429` | As many runs are going as your plan allows at once | Start the run once one finishes. Retrying straight away won't help | | `monthly_run_limit` | `429` | The month's runs are used up | Runs come back at `resets_at`; add runs or turn on extra runs in **Billing** to go on now | Any other error has the code for its kind of problem: `invalid_request` (`400`), `unauthorized` (`401`), `licence_required` (`402`), `forbidden` (`403`), `not_found` (`404`), `method_not_allowed` (`405`), `conflict` (`409`), `request_too_large` (`413`), `unprocessable_request` (`422`), `internal_error` (`500`) or `service_unavailable` (`503`). ## Common errors ```json { "error": "Missing or malformed Authorization header", "error_code": "missing_api_key" } ``` The request has no `Authorization: Bearer …` header, or it isn't in that form. Returned by [Run a workflow](https://docs.lodol.com/docs/api/runs.md#run-a-workflow) when its `inputs` don't match the ones the workflow declares. The message names the input, and nothing has started: ```json { "error": "'Monthly Client Report' has no input named 'clinet'. Its inputs are: client, month.", "error_code": "invalid_inputs" } ``` The key lacks the scope the endpoint needs: ```json { "error": "API key lacks required scope(s)", "error_code": "missing_scope" } ``` Or the member who created the key isn't allowed to do it, or has left the workspace: ```json { "error": "The member this API key acts for is not allowed to delete workflows in this workspace", "error_code": "permission_denied" } ``` Over the rate limit, with a `Retry-After` header saying how many seconds to wait: ```json { "error": "Rate limit exceeded. Please wait before trying again.", "error_code": "rate_limited" } ``` Too many runs already in progress. There's no `Retry-After`: start the run once one of them finishes. ```json { "error": "Concurrent execution limit reached. Your plan allows 5 concurrent executions via the API.", "error_code": "concurrency_limit" } ``` The month's runs are used up. `resets_at` says when they come back, `limit` how many runs a month the plan includes, and `extra_runs_spend_limit` how many dollars of extra runs a month the workspace allows (`null` when it pays for none): ```json { "error": "This workspace has used all 1,000 runs its plan includes this month. Runs start again on November 1, or upgrade for more runs now.", "error_code": "monthly_run_limit", "limit": 1000, "extra_runs_spend_limit": null, "resets_at": "2026-11-01T00:00:00+00:00" } ``` ## Retry safely Retry `409`, `5xx`, and `429` with the code `rate_limited`, waiting longer each time. When the response has a `Retry-After` header, wait that many seconds; otherwise back off exponentially. Don't retry other `4xx` responses: the request itself needs fixing. Nor the other `429`s: `concurrency_limit` clears only when a run finishes, and `monthly_run_limit` when the month's runs come back. ```js tab="JavaScript" tab-group="lang" async function lodolRequest(url, options = {}, maxRetries = 3) { for (let attempt = 0; ; attempt++) { const response = await fetch(url, options); const code = response.status === 429 ? (await response.clone().json()).error_code : null; const retryable = response.status === 409 || response.status >= 500 || code === 'rate_limited'; if (!retryable || attempt === maxRetries) return response; const retryAfter = Number(response.headers.get('Retry-After')); const seconds = retryAfter > 0 ? retryAfter : 2 ** attempt; await new Promise((resolve) => setTimeout(resolve, seconds * 1000)); } } ``` ```python tab="Python" import time import requests def lodol_request(method, url, max_retries=3, **kwargs): for attempt in range(max_retries + 1): response = requests.request(method, url, **kwargs) code = response.json().get("error_code") if response.status_code == 429 else None retryable = ( response.status_code == 409 or response.status_code >= 500 or code == "rate_limited" ) if not retryable or attempt == max_retries: return response retry_after = response.headers.get("Retry-After") time.sleep(int(retry_after) if retry_after else 2 ** attempt) ``` Retry a request that starts a run only with an idempotency key, or a timeout could start the same run twice. ## Idempotency `POST` requests, [Run a workflow](https://docs.lodol.com/docs/api/runs.md#run-a-workflow) and [Stop a run](https://docs.lodol.com/docs/api/runs.md#stop-a-run), accept an `Idempotency-Key` header. Lodol remembers the response to the first request with each key, and returns that same response to any later request with the same key, instead of doing the work again. ```bash curl -X POST https://api-prod.lodol.com/api/v1/workflows/665f1a2b3c4d5e6f7a8b9c0d/run-async \ -H "Authorization: Bearer $LODOL_API_KEY" \ -H "Idempotency-Key: order-1042-confirmation" ``` - **Make the key from what the run is for,** such as your own order or event ID, so a retry of the same work always sends the same key. Keys can be up to 256 characters. - **Responses are kept for 24 hours.** A repeated request in that time gets the first response back, with an `X-Idempotent-Replayed: true` header. - **Requests in flight return `409`.** If the first request with a key is still being processed, a second one gets `409 Conflict` with `Retry-After: 5`. - **A key belongs to one request.** Reusing a key for a different request, such as the same workflow with different inputs, returns `422`. `DELETE` requests take no idempotency key. Deleting a workflow or a run that's already gone returns `404`, so check with [Get a workflow](https://docs.lodol.com/docs/api/workflows.md#get-a-workflow) or [Get a run](https://docs.lodol.com/docs/api/runs.md#get-a-run) before retrying a delete that timed out. # Limits > How many requests a workspace can make each minute, the other limits your plan sets, and how to tell when you're close to them. Web page: https://docs.lodol.com/docs/api/limits ## Requests per minute Each workspace can make a set number of API requests per minute, shared by all of its keys: | Plan | Requests per minute | | --- | --- | | Free | 5 | | Builder | 50 | | Pro | 200 | | Enterprise | Custom | Every response says where you stand: | Header | What it holds | | --- | --- | | `X-RateLimit-Limit` | Requests allowed per minute | | `X-RateLimit-Remaining` | Requests left in the current window | | `X-RateLimit-Reset` | Seconds until the window resets | Over the limit, the API returns `429 Too Many Requests` with a `Retry-After` header saying how many seconds to wait. See [Retry safely](https://docs.lodol.com/docs/api/errors.md#retry-safely). ## Runs in progress at once Your plan also limits how many runs can be in progress at the same time, listed as **Workflows running at once** on the [pricing page](https://www.lodol.com/pricing). Starting a run while you're at the limit returns `429` with the code `concurrency_limit`. It has no `Retry-After`, since waiting a moment doesn't help: start the run once one of the others finishes. ## Runs per month Each run you start through the API counts as one of your plan's monthly runs, the same as a run started any other way. When the month's runs are used up, starting a run returns `429` with the code `monthly_run_limit`, and its `resets_at` says when runs come back: the 1st (UTC), unless you add runs or turn on extra runs in **Billing** first. See [What counts as a run](https://docs.lodol.com/docs/concepts/runs.md#what-counts-as-a-run). ## Request size The body of a [Run a workflow](https://docs.lodol.com/docs/api/runs.md#run-a-workflow) request can be at most 500 KB. ## Keys per workspace | Plan | API keys | | --- | --- | | Free | 1 | | Builder | 10 | | Pro | 40 | | Enterprise | Custom | [Pricing](https://www.lodol.com/pricing) always has the current figures for every plan. # Workflows > List the workspace's workflows, read one with its steps, or delete one for good. Web page: https://docs.lodol.com/docs/api/workflows ## The workflow object ```json { "id": "665f1a2b3c4d5e6f7a8b9c0d", "name": "Monthly Client Report", "description": "Builds a client's monthly report and emails it to them", "created_by": "665f1a2b3c4d5e6f7a8b9c0e", "created_at": "2026-01-15T10:30:00+00:00", "updated_at": "2026-01-15T10:30:00+00:00", "last_run_at": "2026-03-17T08:00:00+00:00", "inputs": [ { "name": "client", "type": "text", "required": true, "default": null, "description": "The client to report on" }, { "name": "month", "type": "text", "required": false, "default": "last month", "description": "" } ], "outputs": [ { "name": "total", "type": "number", "description": "The month's sales, in dollars" }, { "name": "summary", "type": "text", "description": "" } ] } ``` Each entry in `inputs` describes one input: Each entry in `outputs` describes one output: ### Workflow IDs and versions Editing a workflow can save a new version of it, but its `id` stays the same, so you can store it and use it to run the workflow later. Every endpoint that takes a workflow ID also accepts the ID of any version of it, such as an ID this API returned before the workflow was edited, and acts on the workflow as it is now. A run records the version that ran as its [`version_id`](https://docs.lodol.com/docs/api/runs.md#the-run-object). --- ## List workflows Returns every workflow in the workspace the API key belongs to. Requires the `workflows:read` scope. ```bash tab="cURL" tab-group="lang" curl https://api-prod.lodol.com/api/v1/workflows \ -H "Authorization: Bearer $LODOL_API_KEY" ``` ```js tab="JavaScript" const response = await fetch('https://api-prod.lodol.com/api/v1/workflows', { headers: { Authorization: `Bearer ${process.env.LODOL_API_KEY}` }, }); const { workflows } = await response.json(); ``` ```python tab="Python" response = requests.get( "https://api-prod.lodol.com/api/v1/workflows", headers={"Authorization": f"Bearer {os.environ['LODOL_API_KEY']}"}, ) workflows = response.json()["workflows"] ``` **Response `200`:** the workspace's [workflow objects](#the-workflow-object). ```json { "workflows": [ { "id": "665f1a2b3c4d5e6f7a8b9c0d", "name": "Monthly Client Report", "description": "Builds a client's monthly report and emails it to them", "created_by": "665f1a2b3c4d5e6f7a8b9c0e", "created_at": "2026-01-15T10:30:00+00:00", "updated_at": "2026-01-15T10:30:00+00:00", "last_run_at": "2026-03-17T08:00:00+00:00", "inputs": [ { "name": "client", "type": "text", "required": true, "default": null, "description": "The client to report on" } ], "outputs": [ { "name": "total", "type": "number", "description": "The month's sales, in dollars" } ] } ] } ``` --- ## Get a workflow Returns one workflow, including its steps as a `program`. Requires the `workflows:read` scope. `:id` can be the workflow's `id` or the ID of any of its versions; the response always describes the workflow as it is now, under its `id`. ```bash tab="cURL" tab-group="lang" curl https://api-prod.lodol.com/api/v1/workflows/665f1a2b3c4d5e6f7a8b9c0d \ -H "Authorization: Bearer $LODOL_API_KEY" ``` ```js tab="JavaScript" const response = await fetch( 'https://api-prod.lodol.com/api/v1/workflows/665f1a2b3c4d5e6f7a8b9c0d', { headers: { Authorization: `Bearer ${process.env.LODOL_API_KEY}` } }, ); const workflow = await response.json(); ``` ```python tab="Python" response = requests.get( "https://api-prod.lodol.com/api/v1/workflows/665f1a2b3c4d5e6f7a8b9c0d", headers={"Authorization": f"Bearer {os.environ['LODOL_API_KEY']}"}, ) workflow = response.json() ``` **Response `200`:** a [workflow object](#the-workflow-object) with one more field, `program`: the workflow's trigger and steps, in the format described in [Workflow format](https://docs.lodol.com/docs/api/workflow-format.md). ```json { "id": "665f1a2b3c4d5e6f7a8b9c0d", "name": "Monthly Client Report", "program": { "kind": "Program", "version": 1, "inputs": [...], "body": [...] }, "...": "the other workflow fields" } ``` --- ## Delete a workflow Deletes a workflow and every version of it, as deleting it in Lodol does. This can't be undone. Requires the `workflows:delete` scope, and the member who created the key must be allowed to delete workflows (the **Delete workflows** role permission); otherwise the request gets `403`. Deleting a workflow: - stops any run of it in progress - stops its trigger or schedule from listening - removes it from its folders - detaches the secrets and values scoped to it, without deleting them - keeps its past runs, which you can still read under [Runs](https://docs.lodol.com/docs/api/runs.md) `:id` can be the workflow's `id` or the ID of any of its versions. ```bash tab="cURL" tab-group="lang" curl -X DELETE https://api-prod.lodol.com/api/v1/workflows/665f1a2b3c4d5e6f7a8b9c0d \ -H "Authorization: Bearer $LODOL_API_KEY" ``` ```js tab="JavaScript" await fetch('https://api-prod.lodol.com/api/v1/workflows/665f1a2b3c4d5e6f7a8b9c0d', { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.LODOL_API_KEY}` }, }); ``` ```python tab="Python" requests.delete( "https://api-prod.lodol.com/api/v1/workflows/665f1a2b3c4d5e6f7a8b9c0d", headers={"Authorization": f"Bearer {os.environ['LODOL_API_KEY']}"}, ) ``` **Response `200`:** ```json { "id": "665f1a2b3c4d5e6f7a8b9c0d", "name": "Monthly Client Report", "deleted": true } ``` A workflow that doesn't exist, or is already deleted, returns `404`. A delete that fails returns `500` and leaves the workflow as it was. Deleting takes no `Idempotency-Key`, so if a delete times out, check with [Get a workflow](#get-a-workflow) before sending it again: it may have gone through. # Runs > Start a workflow run with inputs, follow it to what it hands back, list past runs, stop a run in progress, and delete one. Web page: https://docs.lodol.com/docs/api/runs A run is one pass through a workflow. Runs start in the background: you start one, get its ID straight away, and check it until it finishes. The API calls a run an **execution**, so its ID is `execution_id` and its endpoints are under `/executions`. ## The run object ```json { "execution_id": "683b2c3d4e5f6a7b8c9d0e1f", "workflow_id": "665f1a2b3c4d5e6f7a8b9c0d", "version_id": "6683e1c4b2a9d7f0e5c3a1b2", "workflow_name": "Monthly Client Report", "status": "success", "is_trigger_listener": false, "created_at": "2026-01-15T10:30:00+00:00", "started_at": "2026-01-15T10:30:00+00:00", "completed_at": "2026-01-15T10:30:03+00:00", "error": null, "outputs": { "total": 1250, "summary": "A good month for Acme" } } ``` ### Run statuses | Status | Meaning | Final? | | --- | --- | --- | | `queued` | Started, and about to begin | No | | `running` | Working through its steps | No | | `waiting` | Set aside by a Wait step until its time comes | No | | `awaiting_approval` | Waiting for someone to answer an [approval request](https://docs.lodol.com/docs/guides/approvals.md) | No | | `success` | Reached the end of the workflow | Yes | | `failed` | A step failed and the run stopped there | Yes | | `stopped` | Stopped before it finished | Yes | When you check a run until it's done, wait for one of the final statuses rather than for it to leave `running`: a run waiting on a Wait step or an approval isn't finished. ### Workflows that start themselves A workflow that starts from its own trigger or schedule doesn't run to an end. Running it turns it on, and the run you get back is the workflow listening: `is_trigger_listener` is set, and its status reads `running` for as long as it listens. Each event or scheduled time it catches starts a run of its own, which [List runs](#list-runs) lists beside it. Don't wait for the listening run to finish; follow the runs it starts instead, and [stop](#stop-a-run) it to turn the workflow off. --- ## Run a workflow Starts a run in the background and returns its ID straight away; follow it with [Get a run](#get-a-run). Requires the `workflows:execute` scope, and the member who created the key must be allowed to run workflows (the **Run workflows** role permission). `:id` can be the workflow's `id` or the ID of any of its versions. Either way, the workflow runs as it is now. Each run counts as one of your plan's monthly runs; when they're used up, the request gets `429` with the code `monthly_run_limit` (see [Error codes](https://docs.lodol.com/docs/api/errors.md#error-codes)). ```json { "inputs": { "client": "Acme", "month": "2026-05" } } ``` ### How inputs are checked Each value is converted to its input's declared type: | Type | Accepts | | --- | --- | | `text` | Any value, as text | | `number` | A number, or text that reads as one, such as `"21"` | | `boolean` | `true` or `false`, or text such as `"yes"`, `"no"`, `"on"` or `"0"` | | `list` | A list, or comma-separated text | | `object` | An object | An optional input you leave out takes its default, so a workflow with no required inputs can be run without a body. The run is refused with `400` and the code `invalid_inputs`, before anything starts and with a message naming the input, when: - a value names an input the workflow doesn't declare - a required input has no value - a value can't be converted to its input's type - a value can't be stored with a run, such as `NaN`, `Infinity`, a whole number wider than 64 bits, or lists and objects nested more than 32 levels deep A workflow that starts from its own trigger or schedule takes no inputs, and sending it some returns `400`. Running it without a body turns it on, so it starts listening for its trigger or schedule: the response has `is_trigger_listener` set. See [Workflows that start themselves](#workflows-that-start-themselves). ### Example ```bash tab="cURL" tab-group="lang" curl -X POST https://api-prod.lodol.com/api/v1/workflows/665f1a2b3c4d5e6f7a8b9c0d/run-async \ -H "Authorization: Bearer $LODOL_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: acme-report-2026-05" \ -d '{"inputs": {"client": "Acme", "month": "2026-05"}}' ``` ```js tab="JavaScript" const response = await fetch( 'https://api-prod.lodol.com/api/v1/workflows/665f1a2b3c4d5e6f7a8b9c0d/run-async', { method: 'POST', headers: { Authorization: `Bearer ${process.env.LODOL_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': 'acme-report-2026-05', }, body: JSON.stringify({ inputs: { client: 'Acme', month: '2026-05' } }), }, ); const run = await response.json(); ``` ```python tab="Python" response = requests.post( "https://api-prod.lodol.com/api/v1/workflows/665f1a2b3c4d5e6f7a8b9c0d/run-async", headers={ "Authorization": f"Bearer {os.environ['LODOL_API_KEY']}", "Idempotency-Key": "acme-report-2026-05", }, json={"inputs": {"client": "Acme", "month": "2026-05"}}, ) run = response.json() ``` **Response `202`:** ```json { "execution_id": "683b2c3d4e5f6a7b8c9d0e1f", "workflow_id": "665f1a2b3c4d5e6f7a8b9c0d", "version_id": "6683e1c4b2a9d7f0e5c3a1b2", "status": "queued", "is_trigger_listener": false, "created_at": "2026-01-15T10:30:00+00:00" } ``` **Response `400`:** ```json { "error": "'Monthly Client Report' needs a value for its 'client' input.", "error_code": "invalid_inputs" } ``` --- ## List runs Returns the workspace's runs, newest first, a page at a time. Requires the `executions:read` scope. The runs a [workflow that starts itself](#workflows-that-start-themselves) starts are listed too, beside its listening run. A workflow another run called isn't listed on its own: it's part of the run that called it. ```bash tab="cURL" tab-group="lang" curl "https://api-prod.lodol.com/api/v1/executions?workflow_id=665f1a2b3c4d5e6f7a8b9c0d&limit=10" \ -H "Authorization: Bearer $LODOL_API_KEY" ``` ```js tab="JavaScript" const params = new URLSearchParams({ workflow_id: '665f1a2b3c4d5e6f7a8b9c0d', limit: '10' }); const response = await fetch(`https://api-prod.lodol.com/api/v1/executions?${params}`, { headers: { Authorization: `Bearer ${process.env.LODOL_API_KEY}` }, }); const { executions } = await response.json(); ``` ```python tab="Python" response = requests.get( "https://api-prod.lodol.com/api/v1/executions", headers={"Authorization": f"Bearer {os.environ['LODOL_API_KEY']}"}, params={"workflow_id": "665f1a2b3c4d5e6f7a8b9c0d", "limit": 10}, ) executions = response.json()["executions"] ``` **Response `200`:** a page of [run objects](#the-run-object), each with its `error` but without `outputs`: [Get a run](#get-a-run) has them. ```json { "executions": [ { "execution_id": "683b2c3d4e5f6a7b8c9d0e1f", "workflow_id": "665f1a2b3c4d5e6f7a8b9c0d", "version_id": "6683e1c4b2a9d7f0e5c3a1b2", "workflow_name": "Monthly Client Report", "status": "failed", "is_trigger_listener": false, "created_at": "2026-01-15T10:30:00+00:00", "started_at": "2026-01-15T10:30:00+00:00", "completed_at": "2026-01-15T10:30:03+00:00", "error": "The sales sheet is empty this month." } ] } ``` --- ## Get a run Returns one run, with the `outputs` it handed back once it has succeeded. Requires the `executions:read` scope. Step results are left out unless you ask for them, which keeps checking a run's status quick; they're for looking into how a run reached its result, while `outputs` is the result. ```bash tab="cURL" tab-group="lang" # Check the status curl https://api-prod.lodol.com/api/v1/executions/683b2c3d4e5f6a7b8c9d0e1f \ -H "Authorization: Bearer $LODOL_API_KEY" # Get every step's result once it has finished curl "https://api-prod.lodol.com/api/v1/executions/683b2c3d4e5f6a7b8c9d0e1f?include_step_results=true" \ -H "Authorization: Bearer $LODOL_API_KEY" ``` ```js tab="JavaScript" const response = await fetch( 'https://api-prod.lodol.com/api/v1/executions/683b2c3d4e5f6a7b8c9d0e1f?include_step_results=true', { headers: { Authorization: `Bearer ${process.env.LODOL_API_KEY}` } }, ); const run = await response.json(); ``` ```python tab="Python" response = requests.get( "https://api-prod.lodol.com/api/v1/executions/683b2c3d4e5f6a7b8c9d0e1f", headers={"Authorization": f"Bearer {os.environ['LODOL_API_KEY']}"}, params={"include_step_results": "true"}, ) run = response.json() ``` **Response `200`:** ```json { "execution_id": "683b2c3d4e5f6a7b8c9d0e1f", "workflow_id": "665f1a2b3c4d5e6f7a8b9c0d", "version_id": "6683e1c4b2a9d7f0e5c3a1b2", "workflow_name": "Monthly Client Report", "status": "running", "is_trigger_listener": false, "created_at": "2026-01-15T10:30:00+00:00", "started_at": "2026-01-15T10:30:00+00:00", "completed_at": null, "error": null, "outputs": {} } ``` **Response `200`, with `include_step_results=true`:** ```json { "execution_id": "683b2c3d4e5f6a7b8c9d0e1f", "workflow_id": "665f1a2b3c4d5e6f7a8b9c0d", "version_id": "6683e1c4b2a9d7f0e5c3a1b2", "workflow_name": "Monthly Client Report", "status": "success", "is_trigger_listener": false, "created_at": "2026-01-15T10:30:00+00:00", "started_at": "2026-01-15T10:30:00+00:00", "completed_at": "2026-01-15T10:30:03+00:00", "error": null, "outputs": { "total": 1250, "summary": "A good month for Acme" }, "steps": [ { "index": 0, "status": "completed", "result": "..." }, { "index": 1, "status": "completed", "result": "..." } ] } ``` --- ## Stop a run Stops a run in progress. Steps that already finished aren't undone. Requires the `workflows:execute` scope, and the member who created the key must be allowed to run workflows. A run that has already finished (`success`, `failed` or `stopped`) returns `400` with the code `run_already_finished`: there's nothing to stop, and [Get a run](#get-a-run) says how it ended. ```bash tab="cURL" tab-group="lang" curl -X POST https://api-prod.lodol.com/api/v1/executions/683b2c3d4e5f6a7b8c9d0e1f/stop \ -H "Authorization: Bearer $LODOL_API_KEY" ``` ```js tab="JavaScript" await fetch('https://api-prod.lodol.com/api/v1/executions/683b2c3d4e5f6a7b8c9d0e1f/stop', { method: 'POST', headers: { Authorization: `Bearer ${process.env.LODOL_API_KEY}` }, }); ``` ```python tab="Python" requests.post( "https://api-prod.lodol.com/api/v1/executions/683b2c3d4e5f6a7b8c9d0e1f/stop", headers={"Authorization": f"Bearer {os.environ['LODOL_API_KEY']}"}, ) ``` **Response `200`:** ```json { "execution_id": "683b2c3d4e5f6a7b8c9d0e1f", "status": "stopped" } ``` --- ## Delete a run Deletes a finished run from the history, as deleting it in Lodol does. This can't be undone. Requires the `executions:delete` scope, and the member who created the key must be allowed to delete runs (the **Delete runs** role permission); otherwise the request gets `403`. The runs it started go with it: the runs its trigger started, and the runs of the workflows it called. A run that hasn't finished, or that started one that hasn't, returns `409` with the code `run_not_finished`; [stop it](#stop-a-run) first, or wait for it to finish. Deleting a run doesn't give back the run it [counted as](https://docs.lodol.com/docs/concepts/runs.md#what-counts-as-a-run). ```bash tab="cURL" tab-group="lang" curl -X DELETE https://api-prod.lodol.com/api/v1/executions/683b2c3d4e5f6a7b8c9d0e1f \ -H "Authorization: Bearer $LODOL_API_KEY" ``` ```js tab="JavaScript" await fetch('https://api-prod.lodol.com/api/v1/executions/683b2c3d4e5f6a7b8c9d0e1f', { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.LODOL_API_KEY}` }, }); ``` ```python tab="Python" requests.delete( "https://api-prod.lodol.com/api/v1/executions/683b2c3d4e5f6a7b8c9d0e1f", headers={"Authorization": f"Bearer {os.environ['LODOL_API_KEY']}"}, ) ``` **Response `200`:** ```json { "execution_id": "683b2c3d4e5f6a7b8c9d0e1f", "deleted": true, "started_runs_deleted": 0 } ``` `started_runs_deleted` says how many of the runs it started went with it. A run that doesn't exist, or is already deleted, returns `404`. Deleting takes no `Idempotency-Key`, so if a delete times out, check with [Get a run](#get-a-run) before sending it again: it may have gone through. # Workflow format > How a workflow's trigger and steps are written as JSON, for reading the program that Get a workflow returns. Web page: https://docs.lodol.com/docs/api/workflow-format [Get a workflow](https://docs.lodol.com/docs/api/workflows.md#get-a-workflow) returns a workflow's trigger and steps as `program`: a tree of JSON nodes, each naming what it is with a `kind`. You never need to write one: the editor and your [AI tool](https://docs.lodol.com/docs/build-with-ai.md) do that. This page explains how to read one. ## A program ```json { "kind": "Program", "version": 1, "inputs": [], "body": [] } ``` `body` is the list of statements, run in order. When the workflow starts from a trigger or a schedule, that's the first statement. `inputs` holds the inputs it declares, and `version` is the version of this format, currently `1`. ## An example The Stripe example from these docs, as a program: a trigger, then two app actions that read the payment. ```json { "kind": "Program", "version": 1, "body": [ { "kind": "LetStmt", "name": "payment", "init": { "kind": "NativeCallExpr", "name": "stripe_new_payment_trigger", "args": [{ "kind": "Literal", "value": "succeeded" }] } }, { "kind": "LetStmt", "name": "logged", "init": { "kind": "NativeCallExpr", "name": "api.request", "args": [ { "kind": "Literal", "value": "POST" }, { "kind": "Literal", "value": "/actions/library/google-sheets/append-row" }, { "kind": "Literal", "value": { "spreadsheet_url": "https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZ", "sheet_name": "Revenue", "row_values": [ { "kind": "NowBuiltIn" }, { "kind": "MemberExpr", "obj": { "kind": "VarRef", "name": "payment" }, "key": "description" }, { "kind": "MemberExpr", "obj": { "kind": "VarRef", "name": "payment" }, "key": "amount" } ] } } ] } }, { "kind": "LetStmt", "name": "posted", "init": { "kind": "NativeCallExpr", "name": "api.request", "args": [ { "kind": "Literal", "value": "POST" }, { "kind": "Literal", "value": "/actions/library/slack/send-message" }, { "kind": "Literal", "value": { "text": { "kind": "BinaryExpr", "op": "+", "left": { "kind": "Literal", "value": "New payment: " }, "right": { "kind": "MemberExpr", "obj": { "kind": "VarRef", "name": "payment" }, "key": "description" } } } } ] } } ] } ``` Each step's result is bound to a name with a `LetStmt`: the trigger's event is `payment`, which later steps read with `MemberExpr` nodes. In the editor, those reads are the `@payment.description` references. ## Statements There are 14 kinds of statement: | `kind` | Fields | In the editor | | --- | --- | --- | | `LetStmt` | `name`, `init?` | A step whose result has a name, such as an action or a calculation | | `AssignStmt` | `name`, `value` | Changing a value declared earlier | | `ExprStmt` | `expr` | A step whose result isn't kept | | `BlockStmt` | `body` | A group of statements | | `IfStmt` | `cond`, `thenBranch`, `elseBranch?` | **If**, with **Otherwise** as `elseBranch`. An `elseBranch` that is itself an `IfStmt` makes a chain of cases. | | `ForOfStmt` | `name`, `iterable`, `body` | **Repeat for each item**. `name` is the current item. | | `RepeatStmt` | `count`, `body` | **Repeat N times** | | `WhileStmt` | `cond`, `body` | **Repeat while** | | `BreakStmt`, `ContinueStmt` | | Leave a loop, or skip to its next pass | | `TryStmt` | `tryBranch`, `catchBranch`, `errorName?` | A try section; see [below](#try-sections) | | `StopStmt` | `status`, `message?` | End the run early, with `status` `success` or `failure` | | `WhenStmt` | `time`, `recurrence?`, `timezone?` | A schedule at a time of day; see [below](#schedules) | | `FrequencyStmt` | `value`, `unit` | A schedule every `value` `unit`s (`seconds`, `minutes`, `hours` or `days`) | ### Try sections A `TryStmt` runs `tryBranch`. If a step inside it fails, the rest of `tryBranch` is skipped and `catchBranch` runs instead, with the failure bound under `errorName` (`error` unless set) as `{ "type": …, "message": … }`. The `type` is `action`, `http`, `timeout`, `variable` or `error`. An empty `catchBranch` lets the run carry on after the failure. A `StopStmt` inside a try still ends the run. See [Handle failures](https://docs.lodol.com/docs/guides/failures.md#let-steps-fail-without-stopping-the-run). ### Schedules A `WhenStmt` runs the workflow at `time`: - With `recurrence`, `time` is a time of day (`"08:00"`) and `recurrence` is an iCalendar rule without the `RRULE:` prefix, such as `"FREQ=WEEKLY;BYDAY=MO,WE,FR"` or `"FREQ=MONTHLY;BYMONTHDAY=1"`. - Without it, `time` is the next occurrence of a time of day, or an ISO date and time for a single run. - `timezone` is an IANA name such as `"America/New_York"`. Without one, the schedule follows the workflow owner's time zone. ## Expressions Expressions produce values: | `kind` | Fields | Produces | | --- | --- | --- | | `Literal` | `value` | A fixed value: text, number, boolean, `null`, list or object | | `VarRef` | `name` | A named value: a step's result, the current item of a loop, or an input | | `MemberExpr` | `obj`, `key` | A field of an object, such as `payment.amount` | | `IndexExpr` | `arr`, `idx` | An item of a list by position, from 0; `-1` is the last. A position past the end gives `null`. | | `UnaryExpr` | `op`, `right` | `-` (negate), `+`, or `!` (not) | | `BinaryExpr` | `op`, `left`, `right` | `+` `-` `*` `/` `%` `**`, comparisons `==` `!=` `<` `<=` `>` `>=`, and `&&` `\|\|`. `+` joins text when either side is text. | | `NativeCallExpr` | `name`, `args` | An app action or a trigger; see [Calls](#calls) | | Built-in function | its own fields | See [Built-in functions](#built-in-functions) | ## Calls ### App actions An app action is a `NativeCallExpr` named `api.request` with three arguments: the method, the action's path, and a payload of its inputs. The path names the app and the action: `/actions/library/google-sheets/append-row` is Google Sheets' **Append row**. The keys of the payload are the action's inputs, as listed on its [integration page](https://docs.lodol.com/docs/integrations.md). In the payload, a fixed value is written as plain JSON, and a value worked out at run time is an expression node in its place, like `row_values` and `text` above. Any object in the payload with a string `kind` is evaluated, at any depth. Lodol's own **Request approval** and stored-value steps are calls too, to `/actions/core/review/request-approval` and `/actions/core/variables/get`, `set`, `increment` and `delete`. ### Triggers A trigger is a `NativeCallExpr` named with the trigger's ID, such as `stripe_new_payment_trigger`, in the first statement. Its `args` are its filters, in order. The `LetStmt` around it names the event. Workflows written against older versions of this page may contain `human.*` or `web.*` calls. They do nothing when a workflow runs. To wait for a person, use the [Request approval](https://docs.lodol.com/docs/guides/approvals.md) step. ## Built-in functions A built-in function is a node whose `kind` ends in `BuiltIn`, with named fields that are themselves expressions. Each one is a built-in step in the editor. | `kind` | Fields | In the editor | | --- | --- | --- | | `FormatTextBuiltIn` | `value`, `case` | Format text | | `LowerCaseBuiltIn`, `UpperCaseBuiltIn`, `CapitalizeBuiltIn`, `TitleCaseBuiltIn` | `value` | Format text | | `TrimBuiltIn` | `value`, `mode?` | Trim whitespace | | `TextLengthBuiltIn` | `value` | Count characters | | `WordCountBuiltIn` | `value` | Count words | | `StartsWithBuiltIn` | `string`, `prefix`, `case_sensitive?` | Check start of text | | `EndsWithBuiltIn` | `string`, `suffix`, `case_sensitive?` | Check end of text | | `TextContainsBuiltIn` | `string`, `substring`, `case_sensitive?` | Check if text includes | | `ReplaceBuiltIn` | `string`, `search`, `replace`, `case_sensitive?` | Replace text | | `SplitBuiltIn` | `string`, `delimiter` | Split text | | `JoinBuiltIn` | `list`, `separator` | Join list into text | | `TruncateBuiltIn` | `string`, `length`, `ellipsis?`, `dont_split_words?` | Shorten text | | `ExtractBuiltIn` | `value`, `type`, `pattern?`, `all_matches?` | Extract from text | | `ValidateTextBuiltIn` | `value`, `type`, `region?` | Validate text | | `MatchKeywordsBuiltIn` | `text`, `strong_terms`, `medium_terms?`, `weak_terms?`, `threshold?` | Match keywords | | `StringifyBuiltIn` | `value`, `mode?` | Turns any value into text | | `kind` | Fields | In the editor | | --- | --- | --- | | `CreateListBuiltIn` | `items` | Create a list | | `LengthBuiltIn` | `value` | Count items | | `FirstBuiltIn`, `LastBuiltIn` | `value` | Get first item, Get last item | | `SliceBuiltIn` | `list`, `start`, `end?` | Take part of list | | `SortBuiltIn` | `value`, `direction?` | Sort list | | `ReverseBuiltIn` | `value` | Reverse list | | `ShuffleBuiltIn` | `value` | Shuffle list | | `AppendBuiltIn` | `list`, `value` | Add item to end | | `InsertBuiltIn` | `list`, `index`, `value` | Insert item | | `RemoveAtBuiltIn` | `list`, `index` | Remove item by position | | `MergeBuiltIn` | `first`, `second` | Combine two lists | | `RemoveDuplicatesBuiltIn` | `value` | Remove duplicates | | `ListContainsBuiltIn` | `list`, `value` | Check if list includes item | | `IndexOfBuiltIn` | `list`, `value` | Find item position | | `FilterBuiltIn` | `list`, `condition` | Filter list. In `condition`, the current item is `VarRef` `item`. | | `PluckBuiltIn` | `list`, `field` | Get one field from every item | | `GroupByBuiltIn` | `list`, `field` | Group items by a field | | `RandomSampleBuiltIn` | `list`, `count` | Pick random items | | `kind` | Fields | In the editor | | --- | --- | --- | | `RoundBuiltIn` | `value`, `mode`, `decimals?` | Round number. `mode` is `nearest`, `up` or `down`. | | `FormatNumberBuiltIn` | `value`, `style`, `decimals?` | Format number. `style` is `plain`, `thousands`, `usd`, `eur`, `gbp` or `jpy`. | | `TotalBuiltIn` | `value` | Calculate total | | `MeanBuiltIn` | `value` | Calculate average | | `MedianBuiltIn` | `value` | Find middle value | | `ModeBuiltIn` | `value` | Find most common number | | `MinBuiltIn`, `MaxBuiltIn` | `value` | Find minimum value, Find maximum value | | `AbsoluteValueBuiltIn` | `value` | Calculate absolute value | | `RandomIntegerBuiltIn` | `min`, `max` | Pick a random number | | `RandomRealBuiltIn` | `min`, `max` | Pick a random decimal number | | `RandomIdBuiltIn` | `format?` | Generate a unique ID | | `kind` | Fields | In the editor | | --- | --- | --- | | `NowBuiltIn` | | Current date and time | | `UnixTimestampBuiltIn` | | Current Unix timestamp | | `TodayBuiltIn`, `TomorrowBuiltIn`, `YesterdayBuiltIn` | `timezone?` | Today's, Tomorrow's and Yesterday's date | | `WeekdayNameBuiltIn` | `date` | Day of week | | `DatePartBuiltIn` | `date`, `part` | Get date part | | `FormatDateBuiltIn` | `date`, `format?`, `timezone?` | Make date readable. `format` is `long` (the default), `short`, `date`, `time` or `iso`. | | `AddTimeBuiltIn` | `date`, `amount`, `unit` | Add or subtract time. `unit` includes `weekdays`, which skips weekends. | | `DateDifferenceBuiltIn` | `start`, `end`, `unit` | Difference between dates | | `TimeUntilBuiltIn` | `date` | Time until date | | `IsHolidayBuiltIn`, `IsBusinessDayBuiltIn` | `date`, `region?` | Check holiday, Check business day | | `IsWeekendBuiltIn` | `date` | Check weekend | | `kind` | Fields | In the editor | | --- | --- | --- | | `AgentBuiltIn` | `task`, `model`, `instructions?`, `tools?`, `output_schema?`, `files?`, `max_steps?`, `fallback_model?`, `memory_key?`, `memory_window?` | [Run an AI agent](https://docs.lodol.com/docs/guides/ai-agents.md) | | `CallWorkflowBuiltIn` | `workflow_id`, `workflow_name?`, `inputs?` | [Run a workflow](https://docs.lodol.com/docs/guides/sub-workflows.md) | | `HttpRequestBuiltIn` | `url`, `method?`, `headers?`, `query?`, `body?`, `body_format?`, `response_format?`, `on_error?` | [Call an API](https://docs.lodol.com/docs/guides/http-and-code.md#call-an-api) | | `RunPythonBuiltIn` | `code`, `inputs?`, `output_type?` | [Run Python code](https://docs.lodol.com/docs/guides/http-and-code.md#run-python-code) | | `WaitBuiltIn` | `seconds`, `unit?` | Wait. `seconds` is the amount, in `unit`. | | `DisplayBuiltIn` | `value` | Output value |