# Build with AI Source: https://docs.nelkap.com/build-with-ai Give your coding assistant the Nelkap docs, so the workflows it writes follow them. A coding assistant writes better workflows when it can read the docs. These docs are ready for it: your assistant can search them through an MCP server, read any page as Markdown, or take the whole site in one file. ## MCP server The docs MCP server lets your assistant search these docs and read their pages while it works, so its code follows the current SDK. It needs no account and no key. Its URL is: ```text theme={"theme":{"light":"light-plus","dark":"dark-plus"}} https://docs.nelkap.com/mcp ``` ### Claude Code Run this command in your project: ```bash theme={"theme":{"light":"light-plus","dark":"dark-plus"}} claude mcp add --transport http nelkap \ https://docs.nelkap.com/mcp ``` To share the server with your team, add it to the project's `.mcp.json` file instead, and commit that file: ```bash theme={"theme":{"light":"light-plus","dark":"dark-plus"}} claude mcp add --transport http nelkap --scope project \ https://docs.nelkap.com/mcp ``` Run `claude mcp list` to check that it's connected. ### Cursor Open the menu at the top of any page of these docs, and choose Connect to Cursor. Or add the server to `.cursor/mcp.json` in your project: ```json .cursor/mcp.json theme={"theme":{"light":"light-plus","dark":"dark-plus"}} { "mcpServers": { "nelkap": { "url": "https://docs.nelkap.com/mcp" } } } ``` ### VS Code Open the menu at the top of any page of these docs, and choose Connect to VS Code. Or run MCP: Add Server from the Command Palette, choose HTTP, and enter the server's URL. ### Other assistants Any assistant that connects to remote MCP servers over HTTP can use the same URL. ## Single pages Each page has a menu at its top. It copies the page as Markdown, ready to paste into a chat, or opens the page in ChatGPT, Claude, or Perplexity, to ask questions about it. Every page is also served as Markdown: add `.md` to its address, like [`https://docs.nelkap.com/quickstart.md`](https://docs.nelkap.com/quickstart.md). ## Full docs Two files cover every page: | File | What it holds | | - | - | | [`llms.txt`](https://docs.nelkap.com/llms.txt) | Every page, with its link and a one-line summary. | | [`llms-full.txt`](https://docs.nelkap.com/llms-full.txt) | The full text of every page, in one file. | Give your assistant `llms.txt` to let it find the right page, or `llms-full.txt` to give it everything at once. ## Review generated code Before you deploy a workflow your assistant wrote, check that: * Each call to another service and each write to your database is inside a step. See [What to put in a step](/concepts/steps#what-to-put-in-a-step). * Nothing outside steps reads the time, draws a random ID, or calls an API. See [Determinism](/concepts/steps#determinism). * Steps that charge, email, or write pass an idempotency key, or check before they act. See [Idempotency](/guides/idempotency). # Changelog Source: https://docs.nelkap.com/changelog See what changed in every release. **0.1.0** First release The first version of `@nelkap/sdk`, the JavaScript SDK for Nelkap. * Register your workflows with [`registerWorkflows`](/reference/register-workflows). Each one can start from an event or on a schedule, and set how many of its runs go at once. * Save each piece of work with [`step.run`](/reference/step-run). A run that stops picks up from its last saved step. * Pause a run for a duration with [`step.sleep`](/reference/step-sleep), or until an event arrives with [`step.wait`](/reference/step-wait). * Record a score of the run with [`step.score`](/reference/step-score). * Start a run from your code with [`run`](/reference/run), and emit an event with [`emit`](/reference/emit). * Answer Nelkap's calls from one route with [`handle`](/reference/handle). # How Nelkap works Source: https://docs.nelkap.com/concepts/how-it-works The execution model behind every run: who runs your code, how a run moves forward call by call, and what Nelkap guarantees. Nelkap makes an ordinary async function durable. The function can stop at any point, for a crash, a deploy, or a pause of several months, and resume exactly where it stopped. This page explains the model behind it. Knowing it tells you how to write workflows that behave the way you expect. ## Architecture Three parts work together. Your code never leaves your app. | Part | Where it runs | What it does | | - | - | - | | Your app | Your servers or your host | Holds your workflows and the route Nelkap calls. | | The SDK | Inside your app | Runs your workflows, saves each step to Nelkap, and answers Nelkap's calls. | | Nelkap | Nelkap's cloud | Stores every run, step, and event. Decides when each run is due, and calls your app to execute it. | Nelkap keeps the state of each run: its payload, the result or error of each step, and what the run is waiting for. Your app keeps nothing between two calls, so it can restart, scale out, or be redeployed at any time. ## Calls Nelkap executes a run through calls. A call is one POST request from Nelkap to your project's endpoint, the route where your app passes requests to [`nelkap.handle`](/reference/handle). Each call carries: | Part | Content | | - | - | | `Authorization` header | `Bearer` followed by your project's secret. `handle` turns down a call without it. | | `workflowName` | The workflow to run. | | `runId` | The run's ID. | | `payload` | The run's payload, the argument your workflow receives. | | `steps` | Every step the run has saved so far, with its result or error. | `handle` answers `202` at once, then runs the workflow in the background. Nelkap never waits for your workflow to finish, however long it takes. A run takes one call for each stretch of work between two pauses. A workflow that never sleeps or waits runs in a single call. A workflow with one sleep runs in two calls: one before the sleep, one after. ## Run lifecycle Here is the `onboardUser` workflow from the [quickstart](/quickstart). It creates a profile, sleeps for a day, then sends a welcome email: ![The onboardUser run over two calls: on day 1, the profile step runs and the run sleeps; on day 2, the profile step returns its saved result and the welcome step runs.](https://mintcdn.com/holotus/zu12qw3F64kRzbLL/images/run-calls-light.png?fit=max\&auto=format\&n=zu12qw3F64kRzbLL\&q=85\&s=c4ceff345630a4d122f072e7b9790154) 1. Your app starts the run with `nelkap.run`. Nelkap records it as pending. 2. Within a minute, Nelkap calls your endpoint with the run's payload and no saved steps. 3. The `profile` step runs, and the SDK saves its result to Nelkap. 4. The workflow reaches the sleep. The SDK tells Nelkap when the run should wake, and the call ends. Nothing runs in your app for a day. 5. When the day is over, Nelkap calls your endpoint again, this time with the saved `profile` step. 6. Your workflow runs from its first line. The `profile` step returns its saved result without running. The sleep is over, so the `welcome` step runs, its result is saved, and the run completes. Each status a run goes through, from pending to completed, is described in [Runs](/concepts/runs#lifecycle). ## Memoization On every call, the SDK runs your workflow function from its first line. Each time the code reaches a step, the SDK looks for that step among the saved ones: * If the step is saved, it returns its saved result at once, or throws its saved error, without running. * If it isn't, it runs, and its result is saved before the code moves on. Your code therefore moves quickly through everything done in earlier calls, and the real work starts at the first step with no saved result. This is what makes a saved step's work happen only once per run. > [!WARNING] > > Code outside steps runs again on every call, so it must take the same path each time. Anything that can change between two calls, like the current time, a random value, or a database read, belongs in a step. See [Determinism](/concepts/steps#determinism). ## Sleeps and waits A sleep or a wait ends the call. Nelkap keeps the run, and your app has nothing running and nothing in memory, whether the pause lasts a minute or a year: * A sleeping run wakes when its time is up. Nelkap checks the timers every minute. * A waiting run wakes when your app emits an event with the key it waits on. Either way, Nelkap calls your endpoint again, and the run resumes after the pause. See [Sleep](/guides/sleep) and [Wait for an event](/guides/wait). ## Failure recovery While a call runs, the SDK sends Nelkap a heartbeat every minute. Nelkap uses it to tell a long step from a dead app. Each kind of failure is handled for you: | What happens | What Nelkap does | | - | - | | Your app crashes or is redeployed in the middle of a call. | The heartbeat stops. After five minutes without one, Nelkap calls your app again, and the run resumes from its last saved step. | | Your host stops the function at its time limit. | Same as a crash: the run resumes about five minutes later. | | Your endpoint is unreachable, or answers with an error. | Nelkap tries again about a minute and a half later. After three failed attempts, the run fails with `deliveryFailed`. | | The SDK can't reach Nelkap to save a step. | The run stops where it is, and resumes about five minutes later. It doesn't fail. | | A step's function throws. | The error is saved like a result, and your workflow decides what happens next. See [Error handling](/guides/failures). | A step cut off before its result was saved runs again from its start when the run resumes. See [At-least-once execution](/concepts/steps#at-least-once-execution). ## Guarantees | Guarantee | What it means for your code | | - | - | | A saved step never runs again in its run. | A step that charged a card, once saved, never charges it again in that run. | | Every step runs at least once. | A step cut off before it was saved runs again. Make steps with side effects safe to repeat. See [Idempotency](/guides/idempotency). | | Steps are matched in the order your code reaches them. | Steps in loops and in `Promise.all` find their saved results in the right order. | | A run cut off by a crash or a deploy resumes. | You don't write recovery code. The run resumes from its last saved step, on the code your app runs at that moment. | | Every call carries your project's secret. | `handle` runs nothing for a request that doesn't come from Nelkap. | # Runs Source: https://docs.nelkap.com/concepts/runs One execution of a workflow: its lifecycle and statuses, its payload and tags, and how to find, replay, or cancel it. A run is one execution of a workflow, with its own payload, its own saved steps, and its own status. Nelkap records every run as it happens, so you can see what any run did and why it stopped, without adding a single log line to your code. ## Lifecycle A run moves through these statuses: ![A run goes from Pending to Ready to Running. From Running, it either ends as Completed or Failed, or pauses as Sleeping or Waiting and goes back to Pending when it wakes. A run that hasn't started, or is paused, can be Canceled.](https://mintcdn.com/holotus/wXODVU55G3H-UZVw/images/run-statuses-light.png?fit=max\&auto=format\&n=wXODVU55G3H-UZVw\&q=85\&s=5e2e41fb54505de56c78cc15d966e5e9) | Status | What it means | | - | - | | Pending | The run is due, and waits for its turn to go to your app. | | Ready | The run is on its way to your app. | | Running | Your app is executing it. | | Sleeping | The run is paused until its time is up. | | Waiting | The run is paused until an event arrives with its key. | | Completed | Your workflow returned. | | Failed | Your workflow threw an error it didn't catch, or Nelkap stopped the run. The reason shows on the run's page. | | Canceled | Someone canceled the run, or its workflow was removed, before it could end. | Nelkap sends pending runs to your app every minute, so a new run starts within a minute of being created, unless it waits for room under a [concurrency limit](/guides/concurrency). A run that wakes from a sleep or a wait goes back to Pending, and is sent to your app the same way. ## Payload The payload is the input of the run: the value your workflow receives as its argument, on every call. It's any JSON value, up to your plan's request size of 1 to 3 MB. Where it comes from depends on how the run started: | The run was started by | Its payload is | | - | - | | [`nelkap.run`](/triggers/run) | The value passed as its second argument. | | An [event](/triggers/events) | The event's payload. | | A [webhook](/triggers/webhooks) | The JSON body of the call the webhook received. | | A [schedule](/triggers/schedules) | `null`. | | A replay | The payload of the run it replays. | The payload is sent as JSON, so a `Date` reaches your workflow as a string. > [!TIP] > > Keep the payload small, like the ID of a user or an order, and load the rest in a step. The run then works on the latest data, and every call carries less. ## Tags Tags are labels you give a run, so you can find it later, like the customer it works for. Pass them when you start the run: ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} await nelkap.run(onboardUser, 'user_42', { tags: ['team_7', 'beta'], }); ``` A run takes up to 10 tags, each up to 128 characters. An event passes its tags to every run it starts. Runs started by a schedule or a webhook have no tags. A replay keeps the tags of the run it replays. ## Triggers Each run records how it started, shown as its trigger: | Trigger | Started by | | - | - | | API | [`nelkap.run`](/triggers/run) in your code. | | Event | An event emitted with [`nelkap.emit`](/triggers/events), or received by a [webhook](/triggers/webhooks). | | Cron | A [schedule](/triggers/schedules). | | Manual | A [replay](#replay-a-run) from the console. | ## Find a run The Runs page of the console lists runs newest first, with their workflow, project, trigger, status, start, end, duration, and number of steps. Narrow the list down by: * Workflow, tag, trigger, or status. * Period: the last hour, 24 hours, 3 days, 7 days, 15 days, or 30 days. * Project, chosen at the top of the page. For example, to see the failed runs of a customer this week, filter by their tag, by the Failed status, and by the last 7 days. The Overview page gives the big picture: the number of projects, workflows, runs, and events, the success rate of recent runs, and the last runs to start. ## Run details Open a run to see everything about it: * Its payload, as your workflow received it. * Its error, if it failed: the message your code threw, or the reason Nelkap stopped it. * Its ID, its tags, and the event that started it, if any. * When it was created, sent to your app, started, and ended, and how long it took. * Its status. A waiting run also shows the key it's waiting on. Below, each step is listed in the order it was saved, with its type, its score if it's a score, when it started and ended, and how long it took. Open a step to see what it returned, or the error it threw. ## Replay a run Replay an ended run to run it again, for example once you've fixed the bug that made it fail. Nelkap starts a new run of the same workflow, with the same payload and tags, and its trigger shows as Manual. Every step runs from the start, and the original run stays as it was, so you can compare the two. * You can replay a run that completed, failed, or was canceled. * Its workflow must still be registered. * A run started by an event is replayed with the event's payload, as a run of its own. The replay doesn't start the other workflows that listened for the event. > [!WARNING] > > A replay runs every step again, including the steps that sent emails or charged cards in the original run. Make those steps safe to repeat before you replay. See [Idempotency](/guides/idempotency). ## Cancel a run Cancel a run to make sure it never resumes. You can cancel a run that is pending, ready, sleeping, or waiting. Once canceled, it's never sent to your app again. A running run can't be canceled, since your app is executing it. To stop all the runs of a workflow, remove the workflow from [`registerWorkflows`](/concepts/workflows#add-and-remove-workflows): its runs that haven't started yet, or are sleeping or waiting, are canceled at once. ## Permissions | Action | Role | | - | - | | See runs and their steps | Every member of the workspace | | Replay or cancel a run | Member or above | See [Members and roles](/manage/members). ## Retention An ended run stays in your project, with its steps, for the history of your plan: | Plan | History | | - | - | | Starter | 1 day after the run ends | | Pro | 7 days after the run ends | | Business | 15 days after the run ends | Runs still in progress are kept for as long as they last, even a year of waiting. To keep ended runs longer, [export them](/manage/exports). # Steps Source: https://docs.nelkap.com/concepts/steps The units of work Nelkap saves: the four step types, what to put in a step, how steps are matched, and the rules that keep a workflow durable. A step is a piece of your workflow that Nelkap saves. Once a step is saved, its work is never done again in that run: every later call gets the saved result back. Steps are what let a run stop at any point and resume where it stopped. ## Step types | Step | What it does | Reference | | - | - | - | | `step.run` | Runs a function and saves what it returns, or the error it threw. | [`step.run`](/reference/step-run) | | `step.sleep` | Pauses the run for a duration, like `3 days`. | [`step.sleep`](/reference/step-sleep) | | `step.wait` | Pauses the run until an event arrives with its key, and returns that event's payload. | [`step.wait`](/reference/step-wait) | | `step.score` | Records a number or a yes or no about the run, charted over time. | [`step.score`](/reference/step-score) | Every step type counts toward the run's limit of 100 steps. ## What to put in a step Put in a `step.run` any work you don't want done twice, and anything a later step needs: * A call to another service: charging a card, sending an email, calling a model. * A write to your database. * A read whose result decides what the workflow does next. * A slow computation whose result you need later. Code outside steps is fine for quick work that gives the same result every time, like building a message from data you already have. > [!TIP] > > Give each step one side effect. A step that charges a card and then sends a receipt runs both again if it's cut off after the charge. Two steps, `charge` and `receipt`, each run once. ## Step names On every call, the SDK matches the steps your code reaches with the saved ones, by type and by name, in the order your code reaches them: * A name is up to 128 characters, shown on the run's page. Spaces at its start and end are dropped. * A name can repeat. In a loop that calls `step.run('send')` three times, the first call gets the first saved `send`, the second gets the second, and so on. * A sleep is named after its duration, like `1 day`. A wait is named after its key. Name steps after what they do, like `charge` or `welcome email`. In a loop, include the item, like `batch 3`, so the run's page shows which step handled what. ## Determinism Code outside steps runs again on every call, so it must take the same path each time. Anything that can change between two calls belongs in a step: the current time, a random value, or what a database or an API answers. ```ts Avoid theme={"theme":{"light":"light-plus","dark":"dark-plus"}} // A new ID on every call: a resumed run sees // a different invoice than the first call did. const invoiceId = crypto.randomUUID(); await nelkap.step.run('invoice', () => createInvoice(invoiceId, orderId), ); ``` ```ts Prefer theme={"theme":{"light":"light-plus","dark":"dark-plus"}} // The ID is saved, so every call gets the same one. const invoiceId = await nelkap.step.run('invoice id', () => crypto.randomUUID(), ); await nelkap.step.run('invoice', () => createInvoice(invoiceId, orderId), ); ``` Without the step, a run that resumes after a crash draws a new ID, and can create a second invoice. | Changes between calls | Put it in a step | | - | - | | `Date.now()`, `new Date()` | `step.run('now', () => new Date().toISOString())` | | `Math.random()`, `crypto.randomUUID()` | `step.run('id', () => crypto.randomUUID())` | | A database read | `step.run('user', () => db.user.findUnique(...))` | | An API call | `step.run('rates', () => fetchRates())` | ## Serialization A step's result is saved as JSON. The SDK passes it through JSON on the call where the step runs too, so your code gets the same value on the first call as on every later one: | Your function returns | The step returns | | - | - | | A string, number, boolean, `null`, array, or plain object | The same value. | | `undefined` | `null`. | | A `Date` | Its ISO string, like `2026-01-15T09:00:00.000Z`. | | A class instance | A plain object with its own fields, without its methods. | | A `Map` or a `Set` | An empty object, `{}`. | | A `BigInt` | Nothing: the step fails, since JSON can't hold a `BigInt`. | Each result must also fit your plan's request size, 1 to 3 MB, and a run holds up to 32 MB of saved steps in all. Return only what later steps need: an order ID rather than the whole order keeps runs small and fast to resume. See [Limits](/limits). ## Parallel steps Steps work in loops, in conditions, and in `Promise.all`. Steps started together run in parallel, and are saved in the order your code called them: ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} const [profile, orders] = await Promise.all([ nelkap.step.run('profile', () => getProfile(userId)), nelkap.step.run('orders', () => getOrders(userId)), ]); ``` See [Loops and parallel steps](/guides/loops) for loops, conditions, and jobs larger than 100 steps. ## Helper functions You can call `nelkap.step` from any function your workflow calls, however deep. The SDK finds the run the code belongs to on its own, so steps can live in helpers shared between workflows: ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} async function notify(userId: string, template: string) { const user = await nelkap.step.run( `user ${template}`, () => getUser(userId), ); await nelkap.step.run(`email ${template}`, () => sendEmail(user.email, template), ); } export async function onboardUser(userId: string) { await notify(userId, 'welcome'); await nelkap.step.sleep('3 days'); await notify(userId, 'tips'); } ``` Calling a step outside a run, for example from a route, throws an error: `nelkap.step can only be called while Nelkap runs a workflow.` ## At-least-once execution A saved step never runs again. A step cut off before its result was saved runs again from its start when the run resumes. That happens when your app crashes or is redeployed in the middle of the step, or when the SDK can't reach Nelkap to save it. For work that leaves a mark outside your app, make a second execution harmless: * Pass an idempotency key built from your data, like `refund_${orderId}`, to the services that accept one. Payment and email providers commonly do. * Otherwise, check first, and skip the work if it's already done. See [Idempotency](/guides/idempotency) for every case. # Workflows Source: https://docs.nelkap.com/concepts/workflows What a workflow is, how to define and register it, and how Nelkap keeps your project's list of workflows in sync with your code. A workflow is an async function of your app that Nelkap runs durably. You write it like any other function, split its work into [steps](/concepts/steps), and register it so Nelkap knows it exists. Each execution of a workflow is a [run](/concepts/runs), with its own payload and its own saved steps. The same workflow can have thousands of runs in progress at once, each at a different point. ## Define a workflow A workflow takes one argument: the run's payload. The payload is any JSON value, chosen when the run starts, like a user ID or an order: ```ts nelkap/workflows/ship-order.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { createLabel, sendTrackingEmail } from '../../shipping'; import { nelkap } from '../client'; type Order = { id: string; address: string; }; export async function shipOrder(order: Order) { const label = await nelkap.step.run('label', () => createLabel(order.address), ); await nelkap.step.run('tracking email', () => sendTrackingEmail(order.id, label.trackingNumber), ); } ``` The type of the argument is the type of the payload. When you start a run with [`nelkap.run`](/triggers/run), TypeScript checks the payload you pass against it. > [!NOTE] > > What a workflow returns isn't saved. To keep a value, return it from a step: it's saved with the step and shown on the run's page. ## Register workflows Nelkap needs to know which workflows your app runs, and what starts each one. Pass them all to `registerWorkflows`, once, when your app starts: ```ts nelkap/index.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { nelkap } from './client'; import { importContacts } from './workflows/import-contacts'; import { onboardUser } from './workflows/onboard-user'; import { sendWeeklyReport } from './workflows/send-weekly-report'; nelkap.registerWorkflows([ { name: 'onboardUser', handler: onboardUser, events: ['user.created'], }, { name: 'sendWeeklyReport', handler: sendWeeklyReport, cron: '0 9 * * 1', }, { name: 'importContacts', handler: importContacts, concurrency: 5, }, ]); export { nelkap, importContacts, onboardUser, sendWeeklyReport }; ``` Each workflow takes these fields: | Field | Required | What it sets | | - | - | - | | `name` | Yes | The name Nelkap knows the workflow by, unique in the project, up to 256 characters. | | `handler` | Yes | The workflow function. | | `events` | No | The events that start a run, up to 100. See [Events](/triggers/events). | | `cron` | No | A schedule that starts a run, in UTC. See [Schedules](/triggers/schedules). | | `concurrency` | No | The most runs of this workflow that may execute at once, from 1 to 500. See [Concurrency](/guides/concurrency). | Every workflow can also be started from your code with [`nelkap.run`](/triggers/run), whatever else starts it. The full signature is in the [`registerWorkflows` reference](/reference/register-workflows). `registerWorkflows` sends the list to Nelkap as soon as you call it. If that request fails, the next `run` or `emit` sends it again before its own request, so a run never reaches a workflow Nelkap doesn't know yet. ## Add and remove workflows The list you register becomes your project's list of workflows. Each time your app registers it: * A workflow new to the project is added. * A workflow already there takes its new events, schedule, and concurrency. * A workflow missing from the list is disabled. Its runs that haven't started yet, or are sleeping or waiting, are canceled. * A disabled workflow that's in the list again is enabled again. > [!WARNING] > > Always register every workflow of the project in a single call. A second call with a shorter list disables every workflow it leaves out, and cancels their runs. Renaming a workflow counts as removing the old name and adding a new one: runs in progress under the old name are canceled. To change a workflow's code while runs are in progress, keep its name and see [Versioning](/guides/versioning). ## Names The name is the workflow's identity. Nelkap uses it to tell your app which function to run, and the console uses it to group runs and metrics. > [!TIP] > > Name a workflow after its function, like `onboardUser`, and never change it once runs exist. A stable name keeps runs in progress, schedules, and history attached to the workflow. ## Workflow list The Workflows page of the console lists every workflow your app has registered, with: * Its project, and what starts it: its events and its schedule. * Its concurrency limit, if it has one. * Its status: Enabled, or Disabled once your app stops registering it. * Its last 24 hours: the runs it started, and its success rate, the share of its ended runs that completed rather than failed. Filter the list by name, by trigger, or by status, and select a workflow to see its runs. A disabled workflow leaves the list once its last runs are gone from your history. ## Limits | What | Limit | | - | - | | Workflows per project | 100, 500, or 1,000, depending on your plan | | Workflow name | 256 characters | | Events per workflow | 100, each up to 256 characters | | Concurrency | 1 to 500 | When the list holds more workflows than your plan allows, Nelkap turns it down, and the next `run` or `emit` throws a `NelkapError` with the code `workflowLimitReached`. See [Limits](/limits). # Environments Source: https://docs.nelkap.com/deploy/environments Give each environment its own project, so tests never touch production runs, events, or schedules. Most apps run in more than one place: production for your customers, and staging to try changes first. In Nelkap, each environment is a project. Projects in the same workspace share nothing but the workspace: each has its own endpoint, its own keys, and its own workflows, runs, and events. ## One project per environment Create one project for each environment, like `production` and `staging`. Then, in each environment of your app: 1. Set the project's endpoint to that environment's route, like `https://your-app.com/api/nelkap` for production and `https://staging.your-app.com/api/nelkap` for staging. 2. Set `NELKAP_API_KEY` and `NELKAP_SECRET` to that project's API key and secret. Your code stays the same everywhere. The keys decide which project a run belongs to, and the endpoint decides which app Nelkap calls. ## Isolation With one project per environment: * A run started in staging is called on your staging app, never on production. * An event emitted in staging starts staging workflows and wakes staging runs only. * Staging registers its own list of workflows. Testing a new workflow in staging doesn't add it to production, and removing one in staging doesn't cancel production runs. * Each project has its own schedules, so a scheduled workflow runs once in each environment that registers it. Two limits apply to the whole workspace, across its projects: the runs going at once, and the requests per second. A burst of tests in staging counts toward both. See [Limits](/limits#by-plan). ## Runs per environment Every list of runs, events, and workflows can be narrowed to one project. Choose the project at the top of the page, and every page you open next shows only that environment. ## Testing A typical flow: 1. Deploy your change to staging. 2. Start a run there, with [`run`](/triggers/run) or by emitting the event that starts it. 3. Open the run, and check each step: what it received, what it returned, and how long it took. 4. Deploy to production once it does what you expect. Nelkap calls your app over the internet, so staging must be deployed to a public HTTPS address too. A local address can't receive Nelkap's calls. # Deployment Source: https://docs.nelkap.com/deploy/overview What your app needs in production, how Nelkap calls it, and what happens to runs while you deploy. Nelkap runs your workflows in your own app, so deploying them means deploying your app, the way you already do. There's no separate worker to run and no build step to add. Your app needs three things in production: the SDK's keys, a route Nelkap can reach, and the list of workflows it runs. ## Prerequisites 1. Set your project's endpoint to the URL the route will have, like `https://your-app.com/api/nelkap`. 2. Set `NELKAP_API_KEY` and `NELKAP_SECRET` in your host's environment variables. See [Installation](/installation#add-your-keys). 3. Deploy. When your app starts, `registerWorkflows` sends its list of workflows to Nelkap. Then start a run, and open it from your runs to follow each step. ## Endpoint requirements Nelkap calls your endpoint over the internet, from its own servers. The URL must: * Use HTTPS, on a domain name. An IP address or `localhost` isn't accepted. * Be public. A route behind a sign-in, a VPN, or a firewall that blocks Nelkap can't be reached. * Answer within 30 seconds, without a redirect. `handle` answers at once, so this only matters if something runs before it. If your app checks sign-in on every route, for example in a middleware, leave this one out. `handle` checks your project's secret instead. ## Delivery and retries Each time a run has work to do, Nelkap sends a POST request to your endpoint: * The `Authorization` header carries your project's secret, as `Bearer `. `handle` answers `401` if it doesn't match `NELKAP_SECRET`. * The JSON body names the workflow, and carries the run's ID, its payload, and the steps already saved. `handle` answers `202` at once, then runs the workflow in the background. While the workflow runs, the SDK saves each step as it finishes, and tells Nelkap every minute that the run is still going. If your endpoint doesn't answer with a `2xx` status, or can't be reached, Nelkap tries again about a minute and a half later. After three attempts, the run fails with `deliveryFailed`. Once your endpoint is back, [replay](/concepts/runs#replay-a-run) the run. ## Workflow registration `registerWorkflows` runs each time your app starts, and sends the full list of workflows to Nelkap. The list replaces the one Nelkap had: * New workflows are added. * Workflows you changed take their new events, schedule, and concurrency. * Workflows you removed are turned off, and their runs that haven't started, or are sleeping or waiting, are canceled. On a serverless host, your app starts with the first request it gets after a deploy, not when the deploy ends. Until then, Nelkap keeps the list from before. `run` and `emit` make sure the list is sent before their own request, so what you start from your code is never affected. A change that must apply before then, like a new schedule, waits for that first request: start a run or emit an event from the new deploy to send the list right away. Every instance of your app must register the same list. A project has one endpoint, so all its workflows run in one app: give each app its own project. See [Workspaces and projects](/manage/workspaces). ## Runs during a deploy A deploy can stop your app in the middle of a run. Nothing is lost: * Runs that are sleeping, waiting, or waiting for their turn aren't running anywhere, so the deploy doesn't touch them. * A run cut off in the middle of a call stops sending its signal. About five minutes later, Nelkap calls your app again, now on the new version, and the run picks up from its last saved step. * A step cut off before its result was saved runs again from its start. See [At-least-once execution](/concepts/steps#at-least-once-execution). Every run that goes on after the deploy uses your new code. See [Versioning](/guides/versioning). ## Time limits Nelkap puts no limit on how long a call takes. Your host might. On serverless hosts, a function stops after a set time, and some stop as soon as the response is sent: * Pass your host's `waitUntil` to `handle`, so the work goes on after the response. Each [framework page](/frameworks/overview) shows the right one. * When the host cuts a call off at its limit, the steps already saved are kept, and Nelkap calls your app again about five minutes later to go on from there. * Keep each step shorter than your host's limit. A single step longer than the limit can never finish. On a server that keeps running, like a container or a virtual machine, there's no limit to plan for. ## Secret rotation If your secret leaks, rotate it from your project. Your endpoint then turns every call away until it has the new one, so update `NELKAP_SECRET` and deploy right after. Calls in the meantime are tried again, up to three attempts in about three minutes. A run whose three attempts all fail shows `deliveryFailed`: replay it once the new secret is live. # Vercel Source: https://docs.nelkap.com/deploy/vercel Deploy your workflows on Vercel: keep runs going after the response, give long steps the time they need, and test them on preview deployments. Vercel runs your routes as functions that stop shortly after they answer. Since `handle` answers Nelkap before the workflow runs, three settings make sure your runs get the time they need. ## Background execution Pass Vercel's `waitUntil` to `handle`, so the function keeps running until the workflow's call ends. In Next.js, use [`after`](https://nextjs.org/docs/app/api-reference/functions/after): ```ts app/api/nelkap/route.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { after } from 'next/server'; import { nelkap } from '@/lib/nelkap'; export async function POST(request: Request) { return nelkap.handle(request, { waitUntil: (promise) => after(() => promise), }); } ``` In other frameworks, use [`waitUntil`](https://vercel.com/docs/functions/functions-api-reference/vercel-functions-package#waituntil) from `@vercel/functions`. Each [framework page](/frameworks/overview) shows the exact route. ## Function duration A function on Vercel runs for a limited time, set by your plan and by the route's `maxDuration`. When a call reaches that limit, the steps already saved are kept, and Nelkap calls your app again about five minutes later to go on from there. So a long run is fine: it simply takes several calls. A single step, though, must finish within one call. If a step can take longer than the default, raise the route's limit, in seconds, within what your plan allows: ```ts app/api/nelkap/route.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} export const maxDuration = 300; ``` See [Vercel's function durations](https://vercel.com/docs/functions/configuring-functions/duration) for the limits of each plan. ## Environment variables Add `NELKAP_API_KEY` and `NELKAP_SECRET` to your Vercel project's environment variables, for each environment that runs workflows. Vercel applies them to the next deployment, so deploy again after adding them. ## Production Set your production project's endpoint to your production domain, like `https://your-app.com/api/nelkap`. A custom domain stays the same from one deployment to the next, so the endpoint never needs to change. ## Preview deployments Every preview deployment gets its own URL, but a Nelkap project has a single endpoint. To run workflows on previews, give them a project of their own, like a staging project: 1. Create a staging project in your workspace. 2. Set its endpoint to the URL of the branch you deploy for testing, like `https://your-app-git-staging-your-team.vercel.app/api/nelkap`. That URL always points to the branch's latest deployment. 3. Add the staging project's API key and secret to your Vercel project's Preview environment. See [Environments](/deploy/environments) for how to keep the two apart. ### Deployment Protection When [Deployment Protection](https://vercel.com/docs/deployment-protection) covers your previews, Vercel turns Nelkap's calls away too, since they come without a sign-in. To let them through, create a secret under [Protection Bypass for Automation](https://vercel.com/docs/deployment-protection/methods-to-bypass-deployment-protection/protection-bypass-automation) in your Vercel project, and add it to the end of your staging endpoint, as `?x-vercel-protection-bypass=your-bypass-secret`. Nelkap calls the URL exactly as you set it, so the secret travels with every call. # AI support agent Source: https://docs.nelkap.com/examples/ai-agent Answer support tickets with an AI agent whose every model call and tool call is saved. An AI agent works in a loop: it asks the model, runs the tools the model asks for, and asks again, until it has an answer. A single ticket can take a dozen model calls and a minute or more. If your app restarts in the middle, a plain loop starts over, pays for every model call again, and may repeat a tool call that already changed something. With Nelkap, each model call and each tool call is a step. When the run picks up after a crash or a deploy, the calls already made give back their saved results, and the agent goes on from the first call that hasn't been made. ![The steps of an answerTicket run: a model call that asks for an order, the order lookup, a second model call that writes the answer, the score, and the reply.](https://mintcdn.com/holotus/zu12qw3F64kRzbLL/images/ai-agent-light.png?fit=max\&auto=format\&n=zu12qw3F64kRzbLL\&q=85\&s=9a372f4c34433cf699505677212fcf9e) ## Write the workflow This agent answers a customer's support ticket with Claude, using the [Anthropic SDK](https://www.npmjs.com/package/@anthropic-ai/sdk). It has one tool, which looks up an order. ```ts nelkap/workflows/answer-ticket.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import Anthropic from '@anthropic-ai/sdk'; import { getOrder } from '../../lib/orders'; import { sendTicketReply } from '../../lib/support'; import { nelkap } from '../client'; const anthropic = new Anthropic(); const instructions = 'You answer support tickets for an online store. ' + 'Look up an order whenever the ticket mentions one.'; const tools: Anthropic.Tool[] = [ { name: 'get_order', description: 'Get the status, items, and tracking number of an order.', input_schema: { type: 'object', properties: { orderId: { type: 'string', description: 'The order ID, like ord_42.', }, }, required: ['orderId'], }, }, ]; export async function answerTicket(ticket: { id: string; message: string; }) { const messages: Anthropic.MessageParam[] = [ { role: 'user', content: ticket.message }, ]; for (let turn = 1; turn <= 10; turn++) { const response = await nelkap.step.run('model', () => anthropic.messages.create({ model: 'claude-opus-5-5', max_tokens: 16000, thinking: { type: 'adaptive' }, system: instructions, tools: tools, messages: messages, }), ); messages.push({ role: 'assistant', content: response.content, }); if (response.stop_reason === 'end_turn') { const reply = response.content .flatMap((block) => block.type === 'text' ? [block.text] : [], ) .join('\n'); await nelkap.step.score('turns to answer', turn); await nelkap.step.run('reply', () => sendTicketReply(ticket.id, reply), ); return; } if (response.stop_reason !== 'tool_use') { throw new Error( `The model stopped with ${response.stop_reason}.`, ); } const toolResults: Anthropic.ToolResultBlockParam[] = []; for (const block of response.content) { if (block.type === 'tool_use') { const { orderId } = block.input as { orderId: string }; const order = await nelkap.step.run('get order', () => getOrder(orderId), ); toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: JSON.stringify(order), }); } } messages.push({ role: 'user', content: toolResults }); } throw new Error('The agent found no answer in 10 turns.'); } ``` On each turn, the workflow: 1. Calls the model with the conversation so far, in a step named `model`. 2. If the model has its answer, records how many turns it took, sends the reply, and ends. 3. If the model asks for orders, looks up each one in a step named `get order`, and adds the results to the conversation for the next turn. Replace `getOrder` and `sendTicketReply` with your own functions. ### Restarts The conversation itself isn't saved. On every call, the workflow rebuilds it from the saved steps: * Steps of the same name are matched in order. When the run picks up, the first `model` step gives back the first answer, the second gives back the second, and so on. The same goes for `get order`. * So the loop goes through the turns already made in an instant, without calling the model or your tools again. The model is called again only from the first turn with no saved answer. ### Guardrails * The loop stops after 10 turns, so a confused agent can't run forever. A run holds up to 100 steps. * If the model stops for any other reason, like reaching `max_tokens`, the run fails, and the reason shows on its page. ## Start a run for each ticket Register the workflow, and cap how many tickets are answered at once to stay within your model provider's rate limits: ```ts nelkap/index.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { nelkap } from './client'; import { answerTicket } from './workflows/answer-ticket'; nelkap.registerWorkflows([ { name: 'answerTicket', handler: answerTicket, concurrency: 10, }, ]); export { nelkap, answerTicket }; ``` At most ten tickets are answered at once. The others wait their turn, in the order they arrived. Then start a run when a ticket arrives: ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} await nelkap.run( answerTicket, { id: 'tkt_93', message: 'Where is my order ord_42?' }, { tags: ['support'] }, ); ``` ## View the runs Open the run in your project's runs. Each model call and each order lookup is a step, with exactly what it returned. When an answer looks wrong, you can read the turn where the agent went off track. The `turns to answer` score is charted over time, so you can see whether a change to the prompt or the tools makes the agent faster. ## Going further * Add more tools, each run in its own step. * Pause for a person's approval before sending the reply, with [`step.wait`](/reference/step-wait), as in the [refund approval example](/examples/refund-approval). * Record more scores, like whether the customer reopened the ticket. # Large import Source: https://docs.nelkap.com/examples/large-import Import a file of any size in batches, a few at a time, and email the user once the last batch is in. A customer uploads a file of 80,000 contacts. Importing it in one request takes too long, and if it stops at row 50,000, starting over imports the first half twice. With Nelkap, the import is split into batches, and each batch gets its own run. Batches go a few at a time, so your database never takes the whole file at once. A batch that stops goes on by itself, and the user gets an email when the last one is in. ![An import of 80 batches: a grid of batch runs, most completed, a few running, the rest waiting their turn, with 52 of 80 done.](https://mintcdn.com/holotus/-VkqCXx08dglIr0L/images/large-import-light.png?fit=max\&auto=format\&n=-VkqCXx08dglIr0L\&q=85\&s=17effdf6b3c2f58ef8c4ec7e72090fb4) ## Write the workflows The import takes two workflows: one that starts the batches, and one that imports a batch. ```ts nelkap/workflows/import-contacts.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { sendImportDoneEmail } from '../../lib/email'; import { importRows, markBatchDone, splitIntoBatches, } from '../../lib/imports'; import { nelkap } from '../client'; type ContactImport = { importId: string; email: string }; export async function importContacts( contactImport: ContactImport, ) { const batchIds = await nelkap.step.run('batches', () => splitIntoBatches(contactImport.importId, 1_000), ); for (const [index, batchId] of batchIds.entries()) { await nelkap.step.run(`start batch ${index}`, () => nelkap.run( importBatch, { ...contactImport, batchId }, { tags: [contactImport.importId] }, ), ); } } export async function importBatch( batch: ContactImport & { batchId: string }, ) { await nelkap.step.run('import', () => importRows(batch.batchId), ); const batchesLeft = await nelkap.step.run('progress', () => markBatchDone(batch.importId, batch.batchId), ); if (batchesLeft === 0) { await nelkap.step.run('done email', () => sendImportDoneEmail( batch.email, batch.importId, `import_done_${batch.importId}`, ), ); } } ``` `importContacts` takes the import and the user's email address: 1. It splits the file into batches of 1,000 rows, and saves their IDs. 2. It starts a run of `importBatch` for each batch. Each start is a step, so a run that picks up halfway doesn't start the same batch twice. Every batch run is tagged with the import's ID. `importBatch` takes one batch: 1. It imports the batch's rows. 2. It marks the batch as done, and counts the batches left. 3. If none is left, it emails the user that the import is done. ## Helper functions Replace the functions from `lib` with your own. Three details keep the import exact: * `importRows` writes each contact with an upsert, keyed by its email address. A batch cut off and run again then updates the same contacts instead of adding them twice. * `markBatchDone` marks the batch as done in your database, then returns how many batches of the import aren't done. Marking a batch done twice changes nothing, so the count stays right when the step runs again. * `sendImportDoneEmail` passes an idempotency key to your email provider. Two batches that finish at the same moment can both see no batch left, and the key makes sure the user gets one email. The same table shows the user the import's progress in your app: the batches done, out of the total. ## Start the import Register both workflows. `importBatch` gets a concurrency limit, so at most 10 batches run at once: ```ts nelkap/index.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { nelkap } from './client'; import { importBatch, importContacts, } from './workflows/import-contacts'; nelkap.registerWorkflows([ { name: 'importContacts', handler: importContacts }, { name: 'importBatch', handler: importBatch, concurrency: 10 }, ]); export { importContacts, nelkap }; ``` Then start a run once the file is uploaded: ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} await nelkap.run( importContacts, { importId: contactImport.id, email: user.email }, { tags: [contactImport.id] }, ); ``` ## View the runs Filter your project's runs by the import's ID: the tag lists the import's run and every batch run. Ten batches run at a time, and the others show as pending until a place frees up. If a batch fails, its run shows the error, and the import waits for it. Fix the cause, and [replay](/concepts/runs#replay-a-run) the batch's run: it imports the batch, and sends the email if it was the last one. ## Larger files `importContacts` holds one step per batch, and a run holds up to 100 steps. With batches of 1,000 rows, that's up to 99,000 rows. For more, make the batches bigger, or add a level: start a run per group of batches, and let each of those start its batches. See [Loops and parallel steps](/guides/loops#fan-out). ## Going further * Tell the user when a batch fails: catch the [`StepError`](/guides/failures#step-errors) around the import step, and email them before you throw it again. * Record the share of valid rows in each batch with [`step.score`](/reference/step-score), to chart the quality of your imports over time. # Refund approval Source: https://docs.nelkap.com/examples/refund-approval Hold a refund until a manager approves it, then refund or explain, however long the answer takes. Some work can't go on without a person's decision. Here, a customer asks for a refund, and a manager must approve it first. The answer might come in five minutes or in two weeks. With Nelkap, the run asks the manager, then waits. While it waits, nothing runs in your app. When the manager answers, the run goes on from where it stopped, with the answer in hand, and refunds the payment or tells the customer why not. ![A refundOrder run waiting on the key refund\_ord\_42, woken by a refund.reviewed event with the same key.](https://mintcdn.com/holotus/zu12qw3F64kRzbLL/images/refund-approval-light.png?fit=max\&auto=format\&n=zu12qw3F64kRzbLL\&q=85\&s=0458d02a18d63b59df6ebdc60da0a066) ## Write the workflow ```ts nelkap/workflows/refund-order.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { sendCustomerEmail, sendReviewRequest, } from '../../lib/email'; import { refundPayment } from '../../lib/payments'; import { nelkap } from '../client'; type RefundRequest = { orderId: string; customerEmail: string; reason: string; }; type Review = { approved: boolean; note: string }; export async function refundOrder(request: RefundRequest) { await nelkap.step.run('review request', () => sendReviewRequest(request), ); const review = await nelkap.step.wait( `refund_${request.orderId}`, ); if (!review.approved) { await nelkap.step.run('declined email', () => sendCustomerEmail( request.customerEmail, 'refund-declined', review.note, ), ); return; } await nelkap.step.run('refund', () => refundPayment(request.orderId, `refund_${request.orderId}`), ); await nelkap.step.run('refunded email', () => sendCustomerEmail( request.customerEmail, 'refund-done', review.note, ), ); } ``` The workflow takes the refund request as its payload: 1. It emails a manager a link to a review page in your app. 2. It waits on the key `refund_`. The call ends, and the run stays paused until an event arrives with that key. 3. When the event arrives, `step.wait` returns its payload: the manager's review. 4. If the manager declined, the customer gets an email with the manager's note. Otherwise, the payment is refunded, and the customer gets an email saying it's done. Replace the functions from `lib` with your own. Here, `refundPayment` passes an idempotency key to your payment provider, so a refund is never made twice, even if a crash cuts the step off before it's saved. ## Start a run for each request Register the workflow: ```ts nelkap/index.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { nelkap } from './client'; import { refundOrder } from './workflows/refund-order'; nelkap.registerWorkflows([ { name: 'refundOrder', handler: refundOrder }, ]); export { nelkap, refundOrder }; ``` Then start a run when a customer asks for a refund: ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} await nelkap.run(refundOrder, { orderId: 'ord_42', customerEmail: 'ana@example.com', reason: 'Arrived damaged', }); ``` ## Send the manager's answer Your review page posts the manager's decision to a route of your app. The route emits it as an event, with the key the run waits on: ```ts app/api/refunds/review/route.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { nelkap } from '@/nelkap'; export async function POST(request: Request) { const review: { orderId: string; approved: boolean; note: string; } = await request.json(); // Check here that the signed-in user may approve refunds. await nelkap.emit({ event: 'refund.reviewed', payload: { approved: review.approved, note: review.note }, wakeOn: `refund_${review.orderId}`, }); return Response.json({}); } ``` The run waiting on `refund_ord_42` goes on within a minute. The key is built from the order, so the route doesn't need to know the run's ID. An event only wakes the runs already waiting on its key. That's why the workflow asks for the review in the step just before the wait: the run is waiting long before the manager can answer. ## View the runs While the manager thinks it over, the run shows as waiting in your project's runs. If the customer withdraws the request, cancel the run from its page. A run can wait up to a year. ## Going further * Remind the manager: start a second workflow that sleeps a day, then emails them if the refund is still pending. * Listen for `refund.reviewed` in another workflow, for example to keep a record of every decision. # Order fulfillment Source: https://docs.nelkap.com/examples/stripe-orders Ship each order paid through Stripe Checkout, email the tracking link, and ask for a review two weeks later. When a customer pays, a lot has to happen, and exactly once: book the shipment, email the tracking link, and ask for a review once the parcel has had time to arrive. Stripe tells your app about each payment with a webhook, but it may send the same webhook twice, and your app may restart in the middle of an order. With Nelkap, each paid order gets its own run. The run ships the order once, even when Stripe sends the payment twice, and picks up where it stopped if your app restarts. Then it sleeps for two weeks before asking for the review. ![Two fulfillOrder runs for the same order: the first ships it and sleeps before the review email, the second stops after the claim.](https://mintcdn.com/holotus/-VkqCXx08dglIr0L/images/stripe-orders-light.png?fit=max\&auto=format\&n=-VkqCXx08dglIr0L\&q=85\&s=e976586f50a7dda63ff3e6ce3a7747aa) ## Write the workflow ```ts nelkap/workflows/fulfill-order.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { sendEmail } from '../../lib/email'; import { claimOrder, createShipment } from '../../lib/orders'; import { nelkap } from '../client'; export async function fulfillOrder(order: { orderId: string; email: string; }) { const claimId = await nelkap.step.run('claim id', () => crypto.randomUUID(), ); const isClaimed = await nelkap.step.run('claim', () => claimOrder(order.orderId, claimId), ); if (!isClaimed) { return; } const shipment = await nelkap.step.run('shipment', () => createShipment(order.orderId), ); await nelkap.step.run('shipped email', () => sendEmail( order.email, 'order-shipped', { trackingUrl: shipment.trackingUrl }, `shipped_${order.orderId}`, ), ); await nelkap.step.sleep('14 days'); await nelkap.step.run('review email', () => sendEmail( order.email, 'review', {}, `review_${order.orderId}`, ), ); } ``` The workflow takes the paid order as its payload: 1. It draws a random claim ID. The ID is saved, so every call of this run has the same one. 2. It claims the order with that ID. If another run already claimed it, this run stops here: the order is in good hands. 3. It books the shipment, and saves the tracking link. 4. It emails the tracking link to the customer. 5. It sleeps for 14 days, then asks for a review. Replace the functions from `lib` with your own. `claimOrder` is the one that keeps an order from shipping twice. It writes the claim ID on the order only if the order has none yet, and returns whether the order now carries this ID: ```sql theme={"theme":{"light":"light-plus","dark":"dark-plus"}} UPDATE orders SET claim_id = $2 WHERE id = $1 AND (claim_id IS NULL OR claim_id = $2) RETURNING id; ``` The first run to claim the order gets a row back, and so does the same run if it claims again after a restart. A second run for the same order gets nothing. The emails pass an idempotency key to your email provider, so they go out once even if a crash cuts a step off before it's saved. ## Receive Stripe's webhook Register the workflow with the event that starts it: ```ts nelkap/index.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { nelkap } from './client'; import { fulfillOrder } from './workflows/fulfill-order'; nelkap.registerWorkflows([ { name: 'fulfillOrder', handler: fulfillOrder, events: ['order.paid'], }, ]); export { nelkap }; ``` When you create the Checkout Session, pass your order's ID as `client_reference_id`. Then add a route for Stripe's webhook. It checks Stripe's signature, and emits `order.paid` for each completed checkout: ```ts app/api/stripe/route.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import Stripe from 'stripe'; import { nelkap } from '@/nelkap'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); export async function POST(request: Request) { let event: Stripe.Event; try { event = stripe.webhooks.constructEvent( await request.text(), request.headers.get('stripe-signature') ?? '', process.env.STRIPE_WEBHOOK_SECRET!, ); } catch { return new Response('Invalid signature', { status: 400 }); } if (event.type === 'checkout.session.completed') { const session = event.data.object; await nelkap.emit({ event: 'order.paid', payload: { orderId: session.client_reference_id, email: session.customer_details?.email, }, }); } return new Response(null, { status: 200 }); } ``` In your Stripe dashboard, add a webhook endpoint with this route's URL and the `checkout.session.completed` event, and set its signing secret as `STRIPE_WEBHOOK_SECRET`. The route answers Stripe once the event is emitted. If `emit` throws, the route answers with an error, and Stripe sends the webhook again later. See [Stripe's guide to webhooks](https://docs.stripe.com/webhooks). ## View the runs Each paid order shows in your project's runs. Open one to see the claim, the shipment with its tracking link, and the email, then the sleep. Two weeks later, the review email goes out, and the run completes. If Stripe sent the same payment twice, you'll see a second run for it that stopped after the claim, without shipping anything. ## Going further * Tag each run with the order's ID, to find an order's runs in one search. * Wait for your carrier's delivery event with [`step.wait`](/guides/wait), and ask for the review a few days after delivery instead. * Listen for `order.paid` in another workflow, for example to update your stock, without touching this one. # Weekly report Source: https://docs.nelkap.com/examples/weekly-report Every Monday morning, gather last week's numbers and email them to your team. A weekly report is simple work that must happen on time, every week: count the sign-ups, the orders, and the revenue, then send the summary. Run it from a server's crontab, and it's skipped the week that server is down, or sent twice when two servers run it. With Nelkap, the schedule lives with the workflow. Nelkap starts one run each Monday, whatever runs your app and however many instances it has. Each run gathers the numbers in steps, so a run cut off halfway goes on without counting again. ![The runs of sendWeeklyReport, one each Monday at 9:00 UTC, all completed.](https://mintcdn.com/holotus/-VkqCXx08dglIr0L/images/weekly-report-light.png?fit=max\&auto=format\&n=-VkqCXx08dglIr0L\&q=85\&s=4a02b3bd9299d15797cda9b621978255) ## Write the workflow ```ts nelkap/workflows/send-weekly-report.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { sendReportEmail } from '../../lib/email'; import { countOrders, countSignups, startOfLastWeek, sumRevenue, } from '../../lib/metrics'; import { nelkap } from '../client'; export async function sendWeeklyReport() { const weekStart = await nelkap.step.run('week start', () => startOfLastWeek(new Date()).toISOString(), ); const [signups, orders, revenue] = await Promise.all([ nelkap.step.run('signups', () => countSignups(weekStart)), nelkap.step.run('orders', () => countOrders(weekStart)), nelkap.step.run('revenue', () => sumRevenue(weekStart)), ]); await nelkap.step.run('email', () => sendReportEmail( { weekStart, signups, orders, revenue }, `report_${weekStart}`, ), ); } ``` A scheduled run has no payload, so the workflow takes no argument: 1. It works out when last week started. The date is a step, so every call of the run reports on the same week, even one that picks up after midnight. 2. It counts the sign-ups, the orders, and the revenue side by side, as three steps saved on their own. 3. It emails the report, with an idempotency key built from the week, so the email goes out once even if a crash cuts the step off before it's saved. Replace the functions from `lib` with your own queries and your email provider. ## Schedule Register the workflow with its schedule: ```ts nelkap/index.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { nelkap } from './client'; import { sendWeeklyReport } from './workflows/send-weekly-report'; nelkap.registerWorkflows([ { name: 'sendWeeklyReport', handler: sendWeeklyReport, cron: '0 9 * * 1', }, ]); export { nelkap, sendWeeklyReport }; ``` `0 9 * * 1` starts a run every Monday at 9:00 UTC. Once your app has registered it, nothing else in your code starts the report. See [Schedules](/triggers/schedules#cron-syntax) to write your own. ## Test it You don't have to wait for Monday. Start a run from any server code of your app: ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} await nelkap.run(sendWeeklyReport); ``` It runs the same workflow, and the report goes out within a minute. ## View the runs Filter your project's runs by the workflow: each Monday shows one run, with Cron as its trigger. Open one to see the week it covered, the three counts, and the email. If Nelkap couldn't start runs for a while, the Mondays missed meanwhile give a single run, so your team never gets a pile of reports at once. ## Going further * Record the week's conversion rate with [`step.score`](/reference/step-score), to chart it week after week. * Send each customer their own report: list them in a step, and start a run for each one. See [Fan-out](/guides/loops#fan-out). # Onboarding emails Source: https://docs.nelkap.com/examples/welcome-emails Welcome each new user, then send tips three days later to those who haven't created a project. A good onboarding series sends the right email at the right time: a welcome the moment someone signs up, and a nudge a few days later, only if they haven't started. Without durable execution, that takes a queue, a scheduled job that scans your users every day, and a table to remember who got which email. With Nelkap, the whole series is one function that reads from top to bottom. Each new user gets their own run, which sleeps for three days between the two emails without holding your app. ![An onboarding run: the welcome email on day 1, a three-day sleep, then the project count and the tips email on day 4.](https://mintcdn.com/holotus/zu12qw3F64kRzbLL/images/welcome-emails-light.png?fit=max\&auto=format\&n=zu12qw3F64kRzbLL\&q=85\&s=ecbefac368ffd353a70937324923be50) ## Write the workflow ```ts nelkap/workflows/onboarding.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { sendEmail } from '../../lib/email'; import { countProjects } from '../../lib/projects'; import { nelkap } from '../client'; export async function sendOnboardingEmails(user: { id: string; email: string; }) { await nelkap.step.run('welcome email', () => sendEmail(user.email, 'welcome', `welcome_${user.id}`), ); await nelkap.step.sleep('3 days'); const projectCount = await nelkap.step.run( 'project count', () => countProjects(user.id), ); if (projectCount === 0) { await nelkap.step.run('tips email', () => sendEmail(user.email, 'tips', `tips_${user.id}`), ); } } ``` The workflow takes the new user as its payload: 1. It sends the welcome email. The step is saved as soon as the email is sent, so a later call never sends it again. 2. It sleeps for three days. The call ends there, and nothing runs in your app until Nelkap calls it back. 3. It counts the user's projects. The count is a step too, so it's saved: if the run picks up again right after, it keeps the same count instead of asking again. 4. If the user has no project yet, it sends the tips email. Replace `sendEmail` and `countProjects` with your own functions. Here, `sendEmail` takes the address, the template, and an idempotency key that your email provider uses to ignore a second send. That way, an email goes out once even if a crash cuts the step off before it's saved. See [Idempotency](/guides/idempotency). ## Start a run at sign-up Register the workflow with the event that starts it: ```ts nelkap/index.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { nelkap } from './client'; import { sendOnboardingEmails } from './workflows/onboarding'; nelkap.registerWorkflows([ { name: 'sendOnboardingEmails', handler: sendOnboardingEmails, events: ['user.signed_up'], }, ]); export { nelkap }; ``` Then emit the event in your sign-up code, once the user is created: ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} await nelkap.emit({ event: 'user.signed_up', payload: { id: user.id, email: user.email }, }); ``` Each sign-up now starts a run, with the user as its payload. Another workflow that should react to sign-ups, like one that adds the user to your CRM, can listen for the same event without any change to your sign-up code. ## View the runs Open the run in your project's runs. Right after the sign-up, the welcome email step shows what it returned, and the run is sleeping. Three days later, the run counts the projects, sends the tips if needed, and completes. If a user deletes their account in the meantime, cancel their run from its page, and the tips email never goes out. ## Going further * Add more emails, each after its own sleep. The series stays in one function, in the order the user receives it. * Tag each run with the user's plan, to filter the runs by plan later. * Record whether the user had a project after three days with [`step.score`](/reference/step-score), to chart how well your onboarding works over time. # Astro Source: https://docs.nelkap.com/frameworks/astro Add Nelkap to an Astro app rendered on demand, from your keys to your first run. This guide adds Nelkap to an Astro app. By the end, your app will run a workflow that creates a new user's profile, waits a day, and sends them a welcome email, and you'll follow each step in your project. Nelkap calls your app on every run, so the route it calls must be rendered on demand. You need an [adapter](https://docs.astro.build/en/guides/on-demand-rendering/) that runs on Node.js, like `@astrojs/node` or `@astrojs/vercel`. You also need a Nelkap project with its endpoint, an API key, and its secret. If you don't have them yet, follow the first three steps of the [quickstart](/quickstart). **1. Install the SDK** ```bash npm theme={"theme":{"light":"light-plus","dark":"dark-plus"}} npm install @nelkap/sdk ``` ```bash pnpm theme={"theme":{"light":"light-plus","dark":"dark-plus"}} pnpm add @nelkap/sdk ``` ```bash yarn theme={"theme":{"light":"light-plus","dark":"dark-plus"}} yarn add @nelkap/sdk ``` **2. Add your keys** Add your project's API key and secret to `.env`: ```bash .env theme={"theme":{"light":"light-plus","dark":"dark-plus"}} NELKAP_API_KEY=your-api-key NELKAP_SECRET=your-project-secret ``` The API key lets your app start runs and save steps. The secret lets your app check that each call really comes from Nelkap. When you deploy, set the same two variables in your host's settings. **3. Write a workflow** Create `src/lib/nelkap.ts`: ```ts src/lib/nelkap.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { Nelkap } from '@nelkap/sdk'; import { getSecret } from 'astro:env/server'; import { createProfile, sendWelcomeEmail, } from './activities'; export const nelkap = new Nelkap({ apiKey: getSecret('NELKAP_API_KEY'), secret: getSecret('NELKAP_SECRET'), }); export async function onboardUser(userId: string) { await nelkap.step.run('profile', () => createProfile(userId), ); await nelkap.step.sleep('1 day'); await nelkap.step.run('welcome', () => sendWelcomeEmail(userId), ); } nelkap.registerWorkflows([ { name: 'onboardUser', handler: onboardUser }, ]); ``` * [`getSecret`](https://docs.astro.build/en/guides/environment-variables/) reads your keys from the server's environment, or from `.env` while you develop. They never reach the browser. * `onboardUser` is your workflow: an async function that takes the run's payload, here a user ID. * Each `step.run` saves what its function returns. When the run picks up after the sleep, the profile step gives back its saved result instead of creating the profile again. * `step.sleep` pauses the run for a day. Nothing runs in your app during that day. * `registerWorkflows` tells Nelkap which workflows your app runs, as soon as the file is loaded. Replace `createProfile` and `sendWelcomeEmail` with your own functions. **4. Add the route** Create an endpoint at the path of your endpoint, and export a `POST` handler that passes each request to `nelkap.handle`: ```ts src/pages/api/nelkap.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import type { APIRoute } from 'astro'; import { nelkap } from '../../lib/nelkap'; export const prerender = false; export const POST: APIRoute = ({ request }) => nelkap.handle(request); ``` `prerender = false` renders the endpoint on each request instead of once at build time. `handle` checks the secret, answers Nelkap at once, and runs the workflow in the background. If your app checks sign-in on every route, for example in a middleware, leave `/api/nelkap` out: `handle` checks Nelkap's secret instead. **5. Deploy** Deploy your app with its two environment variables, so Nelkap can reach the route over the internet. Your project's endpoint must be the route's exact URL, like `https://your-app.com/api/nelkap`. **6. Start a run** Start the workflow from any server code. Here, a sign-up endpoint starts it once the user is created: ```ts src/pages/api/sign-up.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import type { APIRoute } from 'astro'; import { nelkap, onboardUser } from '../../lib/nelkap'; import { createUser } from '../../lib/users'; export const prerender = false; export const POST: APIRoute = async ({ request, redirect, }) => { const user = await createUser(await request.formData()); await nelkap.run(onboardUser, user.id); return redirect('/welcome'); }; ``` `run` returns as soon as Nelkap has the run, so the sign-up doesn't wait for the workflow. Its second argument, `user.id`, is the run's payload: Nelkap passes it to `onboardUser`. **7. View the run** Open your project's runs. The new run shows the profile step with what it returned, then sleeps. A day later, Nelkap calls your route again, the welcome email goes out, and the run completes. ## Deploy on Vercel On Vercel, your function can stop as soon as the response is sent. Pass [`waitUntil`](https://vercel.com/docs/functions/functions-api-reference/vercel-functions-package#waituntil) from `@vercel/functions` to keep the run going after `handle` answers: ```ts src/pages/api/nelkap.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { waitUntil } from '@vercel/functions'; import type { APIRoute } from 'astro'; import { nelkap } from '../../lib/nelkap'; export const prerender = false; export const POST: APIRoute = ({ request }) => nelkap.handle(request, { waitUntil: waitUntil }); ``` The run then goes on for as long as your function may run. If a call reaches that limit, every step already saved is kept, and Nelkap calls your app again about five minutes later to go on from there. # Express Source: https://docs.nelkap.com/frameworks/express Add Nelkap to an Express app, from your keys to your first run. This guide adds Nelkap to an Express 5 app. By the end, your app will run a workflow that creates a new user's profile, waits a day, and sends them a welcome email, and you'll follow each step in your project. You need a Nelkap project with its endpoint, an API key, and its secret. If you don't have them yet, follow the first three steps of the [quickstart](/quickstart). **1. Install the SDK** ```bash npm theme={"theme":{"light":"light-plus","dark":"dark-plus"}} npm install @nelkap/sdk ``` ```bash pnpm theme={"theme":{"light":"light-plus","dark":"dark-plus"}} pnpm add @nelkap/sdk ``` ```bash yarn theme={"theme":{"light":"light-plus","dark":"dark-plus"}} yarn add @nelkap/sdk ``` **2. Add your keys** Set your project's API key and secret as environment variables wherever your app runs: ```bash .env theme={"theme":{"light":"light-plus","dark":"dark-plus"}} NELKAP_API_KEY=your-api-key NELKAP_SECRET=your-project-secret ``` The SDK reads both values from `process.env`. Node.js doesn't load `.env` on its own: start your app with `node --env-file=.env`, or set the variables in your host's settings. The API key lets your app start runs and save steps. The secret lets your app check that each call really comes from Nelkap. **3. Write a workflow** Create `src/nelkap.ts`: ```ts src/nelkap.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { Nelkap } from '@nelkap/sdk'; import { createProfile, sendWelcomeEmail, } from './activities'; export const nelkap = new Nelkap(); export async function onboardUser(userId: string) { await nelkap.step.run('profile', () => createProfile(userId), ); await nelkap.step.sleep('1 day'); await nelkap.step.run('welcome', () => sendWelcomeEmail(userId), ); } nelkap.registerWorkflows([ { name: 'onboardUser', handler: onboardUser }, ]); ``` * `onboardUser` is your workflow: an async function that takes the run's payload, here a user ID. * Each `step.run` saves what its function returns. When the run picks up after the sleep, the profile step gives back its saved result instead of creating the profile again. * `step.sleep` pauses the run for a day. Nothing runs in your app during that day. * `registerWorkflows` tells Nelkap which workflows your app runs, as soon as the file is loaded. Replace `createProfile` and `sendWelcomeEmail` with your own functions. **4. Add the route** `nelkap.handle` takes a standard web `Request`, while Express gives you its own request object. The route below builds a standard `Request` from it, passes it to `handle`, and sends back the answer: ```ts src/index.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import express from 'express'; import { Readable } from 'node:stream'; import { nelkap } from './nelkap'; const app = express(); app.post('/api/nelkap', async (req, res) => { const request = new Request( new URL( req.originalUrl, `${req.protocol}://${req.get('host')}`, ), { method: 'POST', headers: { authorization: req.get('authorization') ?? '', }, body: Readable.toWeb(req) as ReadableStream, duplex: 'half', }, ); const response = await nelkap.handle(request); res.status(response.status).json(await response.json()); }); app.use(express.json()); app.listen(3000); ``` * The request's body streams straight into `handle`, so a run with many saved steps isn't held back by a body size limit. * Add this route before `app.use(express.json())`. Otherwise Express reads the body first, and `handle` gets an empty one. * `handle` checks the secret, answers Nelkap at once, and runs the workflow in the background. Your server keeps running after the answer, so the run goes on until it completes or pauses. If your app checks sign-in on every route, leave `/api/nelkap` out: `handle` checks Nelkap's secret instead. **5. Deploy** Deploy your app with its two environment variables, so Nelkap can reach the route over the internet. Your project's endpoint must be the route's exact URL, like `https://your-app.com/api/nelkap`. **6. Start a run** Start the workflow from any route of your app. Here, a sign-up route starts it once the user is created: ```ts src/index.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import express from 'express'; import { Readable } from 'node:stream'; import { nelkap, onboardUser } from './nelkap'; import { createUser } from './users'; const app = express(); app.post('/api/nelkap', async (req, res) => { const request = new Request( new URL( req.originalUrl, `${req.protocol}://${req.get('host')}`, ), { method: 'POST', headers: { authorization: req.get('authorization') ?? '', }, body: Readable.toWeb(req) as ReadableStream, duplex: 'half', }, ); const response = await nelkap.handle(request); res.status(response.status).json(await response.json()); }); app.use(express.json()); app.post('/sign-up', async (req, res) => { const user = await createUser(req.body); await nelkap.run(onboardUser, user.id); res.json({ userId: user.id }); }); app.listen(3000); ``` `run` returns as soon as Nelkap has the run, so the sign-up doesn't wait for the workflow. Its second argument, `user.id`, is the run's payload: Nelkap passes it to `onboardUser`. **7. View the run** Open your project's runs. The new run shows the profile step with what it returned, then sleeps. A day later, Nelkap calls your route again, the welcome email goes out, and the run completes. # Hono Source: https://docs.nelkap.com/frameworks/hono Add Nelkap to a Hono app running on Node.js, from your keys to your first run. This guide adds Nelkap to a Hono app served with `@hono/node-server`. By the end, your app will run a workflow that creates a new user's profile, waits a day, and sends them a welcome email, and you'll follow each step in your project. You need a Nelkap project with its endpoint, an API key, and its secret. If you don't have them yet, follow the first three steps of the [quickstart](/quickstart). **1. Install the SDK** ```bash npm theme={"theme":{"light":"light-plus","dark":"dark-plus"}} npm install @nelkap/sdk ``` ```bash pnpm theme={"theme":{"light":"light-plus","dark":"dark-plus"}} pnpm add @nelkap/sdk ``` ```bash yarn theme={"theme":{"light":"light-plus","dark":"dark-plus"}} yarn add @nelkap/sdk ``` **2. Add your keys** Set your project's API key and secret as environment variables wherever your app runs: ```bash .env theme={"theme":{"light":"light-plus","dark":"dark-plus"}} NELKAP_API_KEY=your-api-key NELKAP_SECRET=your-project-secret ``` The SDK reads both values from `process.env`. Node.js doesn't load `.env` on its own: start your app with `node --env-file=.env`, or set the variables in your host's settings. The API key lets your app start runs and save steps. The secret lets your app check that each call really comes from Nelkap. **3. Write a workflow** Create `src/nelkap.ts`: ```ts src/nelkap.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { Nelkap } from '@nelkap/sdk'; import { createProfile, sendWelcomeEmail, } from './activities'; export const nelkap = new Nelkap(); export async function onboardUser(userId: string) { await nelkap.step.run('profile', () => createProfile(userId), ); await nelkap.step.sleep('1 day'); await nelkap.step.run('welcome', () => sendWelcomeEmail(userId), ); } nelkap.registerWorkflows([ { name: 'onboardUser', handler: onboardUser }, ]); ``` * `onboardUser` is your workflow: an async function that takes the run's payload, here a user ID. * Each `step.run` saves what its function returns. When the run picks up after the sleep, the profile step gives back its saved result instead of creating the profile again. * `step.sleep` pauses the run for a day. Nothing runs in your app during that day. * `registerWorkflows` tells Nelkap which workflows your app runs, as soon as the file is loaded. Replace `createProfile` and `sendWelcomeEmail` with your own functions. **4. Add the route** Add a POST route at the path of your endpoint, and pass the request to `nelkap.handle`: ```ts src/index.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { serve } from '@hono/node-server'; import { Hono } from 'hono'; import { nelkap } from './nelkap'; const app = new Hono(); app.post('/api/nelkap', (context) => nelkap.handle(context.req.raw), ); serve({ fetch: app.fetch, port: 3000 }); ``` `context.req.raw` is the standard `Request` that `handle` takes. `handle` checks the secret, answers Nelkap at once, and runs the workflow in the background. Your Node.js server keeps running after the answer, so the run goes on until it completes or pauses. If your app checks sign-in on every route, for example in a middleware, leave `/api/nelkap` out: `handle` checks Nelkap's secret instead. **5. Deploy** Deploy your app with its two environment variables, so Nelkap can reach the route over the internet. Your project's endpoint must be the route's exact URL, like `https://your-app.com/api/nelkap`. **6. Start a run** Start the workflow from any route of your app. Here, a sign-up route starts it once the user is created: ```ts src/index.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { serve } from '@hono/node-server'; import { Hono } from 'hono'; import { nelkap, onboardUser } from './nelkap'; import { createUser } from './users'; const app = new Hono(); app.post('/api/nelkap', (context) => nelkap.handle(context.req.raw), ); app.post('/sign-up', async (context) => { const user = await createUser( await context.req.json<{ email: string }>(), ); await nelkap.run(onboardUser, user.id); return context.json({ userId: user.id }); }); serve({ fetch: app.fetch, port: 3000 }); ``` `run` returns as soon as Nelkap has the run, so the sign-up doesn't wait for the workflow. Its second argument, `user.id`, is the run's payload: Nelkap passes it to `onboardUser`. **7. View the run** Open your project's runs. The new run shows the profile step with what it returned, then sleeps. A day later, Nelkap calls your route again, the welcome email goes out, and the run completes. # Next.js Source: https://docs.nelkap.com/frameworks/nextjs Add Nelkap to a Next.js app with the App Router, from your keys to your first run. This guide adds Nelkap to a Next.js app that uses the App Router. By the end, your app will run a workflow that creates a new user's profile, waits a day, and sends them a welcome email, and you'll follow each step in your project. You need Next.js 15.1 or later, and a Nelkap project with its endpoint, an API key, and its secret. If you don't have them yet, follow the first three steps of the [quickstart](/quickstart). **1. Install the SDK** ```bash npm theme={"theme":{"light":"light-plus","dark":"dark-plus"}} npm install @nelkap/sdk ``` ```bash pnpm theme={"theme":{"light":"light-plus","dark":"dark-plus"}} pnpm add @nelkap/sdk ``` ```bash yarn theme={"theme":{"light":"light-plus","dark":"dark-plus"}} yarn add @nelkap/sdk ``` **2. Add your keys** Add your project's API key and secret to `.env.local`: ```bash .env.local theme={"theme":{"light":"light-plus","dark":"dark-plus"}} NELKAP_API_KEY=your-api-key NELKAP_SECRET=your-project-secret ``` Next.js loads this file into `process.env`, where the SDK reads both values. The API key lets your app start runs and save steps. The secret lets your app check that each call really comes from Nelkap. When you deploy, set the same two variables in your host's settings, like your Vercel project's environment variables. **3. Write a workflow** Create `lib/nelkap.ts`. It holds the Nelkap client, your workflow, and the list of workflows your app runs: ```ts lib/nelkap.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { Nelkap } from '@nelkap/sdk'; import { createProfile, sendWelcomeEmail, } from '@/lib/activities'; export const nelkap = new Nelkap(); export async function onboardUser(userId: string) { await nelkap.step.run('profile', () => createProfile(userId), ); await nelkap.step.sleep('1 day'); await nelkap.step.run('welcome', () => sendWelcomeEmail(userId), ); } nelkap.registerWorkflows([ { name: 'onboardUser', handler: onboardUser }, ]); ``` * `onboardUser` is your workflow: an async function that takes the run's payload, here a user ID. * Each `step.run` saves what its function returns. When the run picks up after the sleep, the profile step gives back its saved result instead of creating the profile again. * `step.sleep` pauses the run for a day. Nothing runs in your app during that day. * `registerWorkflows` tells Nelkap which workflows your app runs, as soon as the file is loaded. Replace `createProfile` and `sendWelcomeEmail` with your own functions. **4. Add the route** Create a route handler at the path of your endpoint, and pass each request to `nelkap.handle`: ```ts app/api/nelkap/route.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { after } from 'next/server'; import { nelkap } from '@/lib/nelkap'; export async function POST(request: Request) { return nelkap.handle(request, { waitUntil: (promise) => after(() => promise), }); } ``` `handle` checks the secret, answers Nelkap at once, and runs the workflow in the background. [`after`](https://nextjs.org/docs/app/api-reference/functions/after) keeps that background work going once the answer is sent, on Vercel as on your own server. If your app checks sign-in on every route, for example in a proxy or a middleware, leave `/api/nelkap` out. Nelkap doesn't sign in: `handle` checks its secret instead. **5. Deploy** Deploy your app with its two environment variables, so Nelkap can reach the route over the internet. Your project's endpoint must be the route's exact URL, like `https://your-app.com/api/nelkap`. **6. Start a run** Start the workflow from any server code: a Server Action, a route handler, or a webhook. Here, a Server Action starts it when someone signs up: ```ts app/actions.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} 'use server'; import { nelkap, onboardUser } from '@/lib/nelkap'; import { createUser } from '@/lib/users'; export async function signUp(formData: FormData) { const user = await createUser(formData); await nelkap.run(onboardUser, user.id); } ``` `run` returns as soon as Nelkap has the run, so the sign-up doesn't wait for the workflow. Its second argument, `user.id`, is the run's payload: Nelkap passes it to `onboardUser`. **7. View the run** Open your project's runs. The new run shows the profile step with what it returned, then sleeps. A day later, Nelkap calls your route again, the welcome email goes out, and the run completes. ## Long runs on Vercel On Vercel, `after` keeps a call going for as long as your function may run. If a call reaches that limit, every step already saved is kept, and Nelkap calls your app again about five minutes later to go on from there. If a single step needs more time than the default limit, raise it on the route with [`maxDuration`](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config/maxDuration), in seconds, within what your Vercel plan allows: ```ts app/api/nelkap/route.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} export const maxDuration = 300; ``` # Nuxt Source: https://docs.nelkap.com/frameworks/nuxt Add Nelkap to a Nuxt app, from your keys to your first run. This guide adds Nelkap to a Nuxt 4 app. By the end, your app will run a workflow that creates a new user's profile, waits a day, and sends them a welcome email, and you'll follow each step in your project. You need a Nelkap project with its endpoint, an API key, and its secret. If you don't have them yet, follow the first three steps of the [quickstart](/quickstart). **1. Install the SDK** ```bash npm theme={"theme":{"light":"light-plus","dark":"dark-plus"}} npm install @nelkap/sdk ``` ```bash pnpm theme={"theme":{"light":"light-plus","dark":"dark-plus"}} pnpm add @nelkap/sdk ``` ```bash yarn theme={"theme":{"light":"light-plus","dark":"dark-plus"}} yarn add @nelkap/sdk ``` **2. Add your keys** Add your project's API key and secret to `.env`: ```bash .env theme={"theme":{"light":"light-plus","dark":"dark-plus"}} NELKAP_API_KEY=your-api-key NELKAP_SECRET=your-project-secret ``` In development, Nuxt loads this file into `process.env`, where the SDK reads both values. The API key lets your app start runs and save steps. The secret lets your app check that each call really comes from Nelkap. Nuxt doesn't read `.env` in production, so set the same two variables in your host's settings when you deploy. **3. Write a workflow** Create `server/utils/nelkap.ts`. Code in the `server` folder only runs on the server, which keeps your keys out of the browser: ```ts server/utils/nelkap.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { Nelkap } from '@nelkap/sdk'; import { createProfile, sendWelcomeEmail, } from './activities'; export const nelkap = new Nelkap(); export async function onboardUser(userId: string) { await nelkap.step.run('profile', () => createProfile(userId), ); await nelkap.step.sleep('1 day'); await nelkap.step.run('welcome', () => sendWelcomeEmail(userId), ); } nelkap.registerWorkflows([ { name: 'onboardUser', handler: onboardUser }, ]); ``` * `onboardUser` is your workflow: an async function that takes the run's payload, here a user ID. * Each `step.run` saves what its function returns. When the run picks up after the sleep, the profile step gives back its saved result instead of creating the profile again. * `step.sleep` pauses the run for a day. Nothing runs in your app during that day. * `registerWorkflows` tells Nelkap which workflows your app runs, as soon as the file is loaded. Replace `createProfile` and `sendWelcomeEmail` with your own functions. **4. Add the route** Create a server route at the path of your endpoint. The `.post` in its name makes it answer POST requests only: ```ts server/api/nelkap.post.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { nelkap } from '#server/utils/nelkap'; export default defineEventHandler((event) => { return nelkap.handle(toWebRequest(event), { waitUntil: (promise) => event.waitUntil(promise), }); }); ``` `toWebRequest` turns Nuxt's request into the standard `Request` that `handle` takes. `handle` checks the secret, answers Nelkap at once, and runs the workflow in the background. [`event.waitUntil`](https://nuxt.com/docs/4.x/directory-structure/server#awaiting-promises-after-response) keeps that background work going once the answer is sent, on serverless hosts as on your own server. If your app checks sign-in on every route, for example in a server middleware, leave `/api/nelkap` out: `handle` checks Nelkap's secret instead. **5. Deploy** Deploy your app with its two environment variables, so Nelkap can reach the route over the internet. Your project's endpoint must be the route's exact URL, like `https://your-app.com/api/nelkap`. **6. Start a run** Start the workflow from any server code. Here, the sign-up route starts it once the user is created: ```ts server/api/sign-up.post.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { nelkap, onboardUser } from '#server/utils/nelkap'; import { createUser } from '#server/utils/users'; export default defineEventHandler(async (event) => { const user = await createUser( await readBody<{ email: string }>(event), ); await nelkap.run(onboardUser, user.id); return { userId: user.id }; }); ``` `run` returns as soon as Nelkap has the run, so the sign-up doesn't wait for the workflow. Its second argument, `user.id`, is the run's payload: Nelkap passes it to `onboardUser`. **7. View the run** Open your project's runs. The new run shows the profile step with what it returned, then sleeps. A day later, Nelkap calls your route again, the welcome email goes out, and the run completes. ## Long runs on serverless hosts On a serverless host, `event.waitUntil` keeps a call going for as long as your function may run. If a call reaches that limit, every step already saved is kept, and Nelkap calls your app again about five minutes later to go on from there. Keep each step shorter than your host's limit. # Overview Source: https://docs.nelkap.com/frameworks/overview How Nelkap reaches your app, and how to add the route it calls in your framework. Nelkap doesn't run your code on its own servers. Your workflows stay in your app, next to your database and your other code. Each time a run has work to do, Nelkap sends a POST request to your app, and the SDK runs the workflow right there. That request arrives at your project's endpoint: one route that you add to your app and that passes each request to `nelkap.handle`. This section shows how to add that route in your framework, from the keys to your first run. ## Choose your framework > **[Next.js](/frameworks/nextjs)** > > A route handler in the App Router. > **[React Router](/frameworks/react-router)** > > A resource route, in framework mode. > **[SvelteKit](/frameworks/sveltekit)** > > A `+server.ts` endpoint. > **[Nuxt](/frameworks/nuxt)** > > A server route in the `server` folder. > **[Astro](/frameworks/astro)** > > An endpoint rendered on demand. > **[Hono](/frameworks/hono)** > > A route on a Hono app running on Node.js. > **[Express](/frameworks/express)** > > A route that turns Express's request into a standard one. ## Other frameworks `nelkap.handle` takes a standard web [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and returns a standard [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response). Any framework that gives you the incoming request in that form works the same way: add a POST route, and return what `handle` returns. ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { nelkap } from './nelkap'; export const handleNelkap = async ( request: Request, ): Promise => { return nelkap.handle(request); }; ``` The SDK needs Node.js 22 or later. ## How `handle` works Each call from Nelkap carries the workflow to run, the run's payload, and the steps already saved. `handle` then does three things: 1. It checks that the call carries your project's secret. If it doesn't, it answers `401`, and nothing runs. 2. It looks for the workflow among the ones you registered. If your app doesn't have it, it answers `404`. 3. It answers `202` right away, then runs the workflow in the background, saving each step as it finishes. Answering first means Nelkap never waits for your workflow, however long it takes. ## Serverless hosts Hosts like Vercel can stop your code as soon as the response is sent. Since `handle` answers before the workflow runs, the run would stop with it. To keep it going, pass your host's `waitUntil` to `handle`: ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} export const handleNelkap = async ( request: Request, waitUntil: (promise: Promise) => void, ): Promise => { return nelkap.handle(request, { waitUntil: waitUntil, }); }; ``` Each framework page shows the right `waitUntil` for its hosts. ## Deployment Your endpoint must be a public HTTPS URL that answers without a sign-in. [Deployment](/deploy/overview) lists what it needs, how Nelkap retries a call, and how your host's time limits play out. # React Router Source: https://docs.nelkap.com/frameworks/react-router Add Nelkap to a React Router app in framework mode, formerly Remix, from your keys to your first run. This guide adds Nelkap to a React Router app in framework mode, the successor of Remix. By the end, your app will run a workflow that creates a new user's profile, waits a day, and sends them a welcome email, and you'll follow each step in your project. You need a Nelkap project with its endpoint, an API key, and its secret. If you don't have them yet, follow the first three steps of the [quickstart](/quickstart). **1. Install the SDK** ```bash npm theme={"theme":{"light":"light-plus","dark":"dark-plus"}} npm install @nelkap/sdk ``` ```bash pnpm theme={"theme":{"light":"light-plus","dark":"dark-plus"}} pnpm add @nelkap/sdk ``` ```bash yarn theme={"theme":{"light":"light-plus","dark":"dark-plus"}} yarn add @nelkap/sdk ``` **2. Add your keys** Set your project's API key and secret as environment variables wherever your app's server runs: ```bash theme={"theme":{"light":"light-plus","dark":"dark-plus"}} NELKAP_API_KEY=your-api-key NELKAP_SECRET=your-project-secret ``` The SDK reads both values from `process.env`. The API key lets your app start runs and save steps. The secret lets your app check that each call really comes from Nelkap. **3. Write a workflow** Create `app/nelkap.server.ts`. The `.server` in its name keeps it, and your keys, out of the browser bundle: ```ts app/nelkap.server.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { Nelkap } from '@nelkap/sdk'; import { createProfile, sendWelcomeEmail, } from './activities.server'; export const nelkap = new Nelkap(); export async function onboardUser(userId: string) { await nelkap.step.run('profile', () => createProfile(userId), ); await nelkap.step.sleep('1 day'); await nelkap.step.run('welcome', () => sendWelcomeEmail(userId), ); } nelkap.registerWorkflows([ { name: 'onboardUser', handler: onboardUser }, ]); ``` * `onboardUser` is your workflow: an async function that takes the run's payload, here a user ID. * Each `step.run` saves what its function returns. When the run picks up after the sleep, the profile step gives back its saved result instead of creating the profile again. * `step.sleep` pauses the run for a day. Nothing runs in your app during that day. * `registerWorkflows` tells Nelkap which workflows your app runs, as soon as the file is loaded. Replace `createProfile` and `sendWelcomeEmail` with your own functions. **4. Add the route** Nelkap sends POST requests, which React Router passes to a route's `action`. A route module with an `action` and no component is a [resource route](https://reactrouter.com/how-to/resource-routes): it answers with what you return instead of rendering a page. Create the route module: ```ts app/routes/api.nelkap.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { nelkap } from '../nelkap.server'; import type { Route } from './+types/api.nelkap'; export async function action({ request }: Route.ActionArgs) { return nelkap.handle(request); } ``` Then add it to your routes, at the path of your endpoint: ```ts app/routes.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { type RouteConfig, index, route, } from '@react-router/dev/routes'; export default [ index('routes/home.tsx'), route('api/nelkap', 'routes/api.nelkap.ts'), ] satisfies RouteConfig; ``` `handle` checks the secret, answers Nelkap at once, and runs the workflow in the background. If your app checks sign-in on every route, leave `/api/nelkap` out: `handle` checks Nelkap's secret instead. **5. Deploy** Deploy your app with its two environment variables, so Nelkap can reach the route over the internet. Your project's endpoint must be the route's exact URL, like `https://your-app.com/api/nelkap`. **6. Start a run** Start the workflow from any server code. Here, the sign-up route's `action` starts it once the user is created: ```ts app/routes/sign-up.tsx theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { redirect } from 'react-router'; import { nelkap, onboardUser } from '../nelkap.server'; import { createUser } from '../users.server'; import type { Route } from './+types/sign-up'; export async function action({ request }: Route.ActionArgs) { const user = await createUser(await request.formData()); await nelkap.run(onboardUser, user.id); return redirect('/welcome'); } ``` `run` returns as soon as Nelkap has the run, so the sign-up doesn't wait for the workflow. Its second argument, `user.id`, is the run's payload: Nelkap passes it to `onboardUser`. **7. View the run** Open your project's runs. The new run shows the profile step with what it returned, then sleeps. A day later, Nelkap calls your route again, the welcome email goes out, and the run completes. ## Deploy on Vercel On Vercel, your function can stop as soon as the response is sent. Pass [`waitUntil`](https://vercel.com/docs/functions/functions-api-reference/vercel-functions-package#waituntil) from `@vercel/functions` to keep the run going after `handle` answers: ```ts app/routes/api.nelkap.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { waitUntil } from '@vercel/functions'; import { nelkap } from '../nelkap.server'; import type { Route } from './+types/api.nelkap'; export async function action({ request }: Route.ActionArgs) { return nelkap.handle(request, { waitUntil: waitUntil }); } ``` The run then goes on for as long as your function may run. If a call reaches that limit, every step already saved is kept, and Nelkap calls your app again about five minutes later to go on from there. # SvelteKit Source: https://docs.nelkap.com/frameworks/sveltekit Add Nelkap to a SvelteKit app, from your keys to your first run. This guide adds Nelkap to a SvelteKit 3 app. By the end, your app will run a workflow that creates a new user's profile, waits a day, and sends them a welcome email, and you'll follow each step in your project. You need a Nelkap project with its endpoint, an API key, and its secret. If you don't have them yet, follow the first three steps of the [quickstart](/quickstart). **1. Install the SDK** ```bash npm theme={"theme":{"light":"light-plus","dark":"dark-plus"}} npm install @nelkap/sdk ``` ```bash pnpm theme={"theme":{"light":"light-plus","dark":"dark-plus"}} pnpm add @nelkap/sdk ``` ```bash yarn theme={"theme":{"light":"light-plus","dark":"dark-plus"}} yarn add @nelkap/sdk ``` **2. Add your keys** Add your project's API key and secret to `.env.local`: ```bash .env.local theme={"theme":{"light":"light-plus","dark":"dark-plus"}} NELKAP_API_KEY=your-api-key NELKAP_SECRET=your-project-secret ``` Then declare both in `src/env.ts`, so SvelteKit can give them to your server code: ```ts src/env.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { defineEnvVars } from '@sveltejs/kit/env'; export const variables = defineEnvVars({ NELKAP_API_KEY: {}, NELKAP_SECRET: {}, }); ``` Variables are private by default, so they never reach the browser. The API key lets your app start runs and save steps. The secret lets your app check that each call really comes from Nelkap. When you deploy, set the same two variables in your host's settings. See [environment variables](https://svelte.dev/docs/kit/environment-variables) in the SvelteKit docs. **3. Write a workflow** Create `src/lib/server/nelkap.ts`. Files in a `server` folder only run on the server, which keeps your keys out of the browser: ```ts src/lib/server/nelkap.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { NELKAP_API_KEY, NELKAP_SECRET, } from '$app/env/private'; import { Nelkap } from '@nelkap/sdk'; import { createProfile, sendWelcomeEmail, } from './activities.js'; export const nelkap = new Nelkap({ apiKey: NELKAP_API_KEY, secret: NELKAP_SECRET, }); export async function onboardUser(userId: string) { await nelkap.step.run('profile', () => createProfile(userId), ); await nelkap.step.sleep('1 day'); await nelkap.step.run('welcome', () => sendWelcomeEmail(userId), ); } nelkap.registerWorkflows([ { name: 'onboardUser', handler: onboardUser }, ]); ``` * The client takes your keys from `$app/env/private`, where SvelteKit provides them. * `onboardUser` is your workflow: an async function that takes the run's payload, here a user ID. * Each `step.run` saves what its function returns. When the run picks up after the sleep, the profile step gives back its saved result instead of creating the profile again. * `step.sleep` pauses the run for a day. Nothing runs in your app during that day. * `registerWorkflows` tells Nelkap which workflows your app runs, as soon as the file is loaded. Replace `createProfile` and `sendWelcomeEmail` with your own functions. **4. Add the route** Create a `+server.ts` file at the path of your endpoint, and export a `POST` handler that passes each request to `nelkap.handle`: ```ts src/routes/api/nelkap/+server.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { nelkap } from '#lib/server/nelkap.js'; import type { RequestHandler } from './$types'; export const POST: RequestHandler = ({ request }) => nelkap.handle(request); ``` `handle` checks the secret, answers Nelkap at once, and runs the workflow in the background. If your app checks sign-in on every route, for example in `hooks.server.ts`, leave `/api/nelkap` out: `handle` checks Nelkap's secret instead. **5. Deploy** Deploy your app with its two environment variables, so Nelkap can reach the route over the internet. Your project's endpoint must be the route's exact URL, like `https://your-app.com/api/nelkap`. **6. Start a run** Start the workflow from any server code. Here, the sign-up page's form action starts it once the user is created: ```ts src/routes/sign-up/+page.server.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { redirect } from '@sveltejs/kit'; import { nelkap, onboardUser } from '#lib/server/nelkap.js'; import { createUser } from '#lib/server/users.js'; import type { Actions } from './$types'; export const actions = { default: async ({ request }) => { const user = await createUser(await request.formData()); await nelkap.run(onboardUser, user.id); redirect(303, '/welcome'); }, } satisfies Actions; ``` `run` returns as soon as Nelkap has the run, so the sign-up doesn't wait for the workflow. Its second argument, `user.id`, is the run's payload: Nelkap passes it to `onboardUser`. **7. View the run** Open your project's runs. The new run shows the profile step with what it returned, then sleeps. A day later, Nelkap calls your route again, the welcome email goes out, and the run completes. ## Deploy on Vercel On Vercel, your function can stop as soon as the response is sent. Pass [`waitUntil`](https://vercel.com/docs/functions/functions-api-reference/vercel-functions-package#waituntil) from `@vercel/functions` to keep the run going after `handle` answers: ```ts src/routes/api/nelkap/+server.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { waitUntil } from '@vercel/functions'; import { nelkap } from '#lib/server/nelkap.js'; import type { RequestHandler } from './$types'; export const POST: RequestHandler = ({ request }) => nelkap.handle(request, { waitUntil: waitUntil }); ``` The run then goes on for as long as your function may run, which you can raise with the adapter's [`maxDuration`](https://svelte.dev/docs/kit/adapter-vercel) option. If a call reaches that limit, every step already saved is kept, and Nelkap calls your app again about five minutes later to go on from there. # Concurrency Source: https://docs.nelkap.com/guides/concurrency Set how many runs of a workflow may execute at once, so a burst of work never overwhelms your app or the services it calls. A thousand sign-ups in a minute start a thousand runs. If each one calls a partner's API that accepts five requests at a time, most of them fail. With a concurrency limit, five runs execute at once, the others wait their turn, and none is lost. ## Set a limit Set `concurrency` when you register the workflow: ```ts nelkap/index.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} nelkap.registerWorkflows([ { name: 'importContacts', handler: importContacts, concurrency: 5, }, ]); ``` At most five runs of `importContacts` execute at once. The limit is a whole number from 1 to 500. ![The importContacts workflow with concurrency 5: five runs are running and count toward the limit, four pending runs wait their turn oldest first, and two sleeping or waiting runs don't count while paused.](https://mintcdn.com/holotus/hIJ_s-I7ldf2FTKj/images/concurrency-queue-light.png?fit=max\&auto=format\&n=hIJ_s-I7ldf2FTKj\&q=85\&s=017192071cfae1b0988309c93d2736a1) ## Use cases | Need | Setting | | - | - | | Stay under what another service accepts, like a partner API that takes five requests at a time. | `concurrency: 5` on the workflow that calls it. | | Spread the load of a big job, like the batches of an import that all write to your database. | A low limit on the workflow that runs each batch. | | Never let two runs overlap, like a nightly sync. | `concurrency: 1`. See [Sequential runs](#sequential-runs). | ## Counted runs A run counts toward the limit from the moment it's sent to your app until it pauses or ends: * A run on its way to your app, or running in it, counts. * A sleeping or waiting run doesn't, since nothing runs while it's paused. It counts again once it wakes up and goes back to your app. * A pending run doesn't count yet. It's the one waiting for its turn. So a limit caps the work your app does at the same time, not the number of runs in progress. ## Queue order Runs over the limit stay pending, and take their turn in the order they were created: the oldest goes first. Nelkap hands out free turns every minute, so a waiting run starts within a minute after a turn frees up. A run woken from a sleep or a wait waits for a turn too. It keeps its place by creation date, so it goes ahead of runs created after it. ## Workspace limit Your workspace also has a limit on the runs executing at once across all its workflows: 50, 100, or 500, depending on your plan. A workflow without `concurrency` only has that limit. A workflow with one gets whichever is lower. See [Limits](/limits#by-plan). ## Change a limit The limit you register replaces the old one at once. Raising it lets more runs start on the next minute. Lowering it never stops a run already executing: the runs over the new limit finish, and no new run starts until the count falls below it. To remove a limit, register the workflow without `concurrency`. ## Sequential runs With `concurrency: 1`, the runs of a workflow execute one after the other, in the order they were created. That's useful for work that must never overlap, like a nightly sync that shouldn't start before the previous one ended: ```ts nelkap/index.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} nelkap.registerWorkflows([ { name: 'syncCatalog', handler: syncCatalog, cron: '0 2 * * *', concurrency: 1, }, ]); ``` > [!NOTE] > > The limit applies to the whole workflow, not to each customer. With `concurrency: 1`, two runs for different customers also wait for each other. ## Queue metrics On the Metrics page, the Backlog chart shows the runs waiting to start, Concurrency the runs executing at once, and Latency how long runs wait before they start. A backlog that keeps growing means the limit is lower than the work coming in. See [Metrics](/monitor/metrics). # Error handling Source: https://docs.nelkap.com/guides/failures What happens when a step or a run fails, how to take another path or try again, and what Nelkap handles for you. Things fail: a card is declined, an API is down, a bug slips through. In Nelkap, a step's failure is saved like any result. Your workflow decides what happens next, and every later call of the run sees the same failure, so the run always takes the same path. ## Step errors If a step's function throws, the SDK saves the error's message and throws a `StepError` in your workflow. You have two choices: * Leave it uncaught. The run fails, and the message shows on its page. * Catch it, and take another path. ```ts nelkap/workflows/charge-order.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { StepError } from '@nelkap/sdk'; import { chargeCard, sendPaymentFailedEmail, shipOrder, } from '../../orders'; import { nelkap } from '../client'; export async function chargeOrder(orderId: string) { try { await nelkap.step.run('charge', () => chargeCard(orderId)); } catch (error) { if (!(error instanceof StepError)) { throw error; } await nelkap.step.run('payment failed email', () => sendPaymentFailedEmail(orderId), ); return; } await nelkap.step.run('shipping', () => shipOrder(orderId)); } ``` Here, a declined card sends an email instead of shipping the order. On every later call, the `charge` step throws the same `StepError` without charging again, so the run never ships an unpaid order. The `StepError` carries the message of the error your function threw, up to 2,048 characters. On the call where the step ran, its `cause` is that error itself. See [Errors](/reference/errors#steperror). ## Retries A failed step stays failed: its saved error comes back on every call. To try the work again, catch the error, sleep, and run a new step with another name: ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} export async function syncInvoice(invoiceId: string) { for (let attempt = 1; attempt <= 3; attempt++) { try { return await nelkap.step.run( `send attempt ${attempt}`, () => sendToAccounting(invoiceId), ); } catch (error) { if ( !(error instanceof StepError) || attempt === 3 ) { throw error; } await nelkap.step.sleep(`${attempt * 10} minutes`); } } } ``` Each attempt is its own step, so each failure is saved, and a run that resumes in the middle skips the attempts already made. The sleeps leave the service time to recover: 10 minutes after the first failure, 20 after the second. After the third failure, the error goes up and the run fails. > [!TIP] > > Retry only what can pass on a second try, like a timeout or a service that's down. A declined card stays declined: catch that error and take another path instead. ## Run failures A run fails when your workflow throws an error it doesn't catch. Its message, up to 2,048 characters, shows on the run's page. Nelkap can also stop a run for a reason of its own: | Reason | What happened | | - | - | | `deliveryFailed` | Your endpoint could not be reached after three attempts. | | `stepLimitReached` | The run reached its limit of 100 steps, or of 32 MB of saved steps. | | `waitExpired` | The run waited more than a year for its event. | ## API errors If Nelkap turns down a request, for example because a workflow isn't registered or a limit is reached, `run` and `emit` throw a `NelkapError` that names the reason. Inside a workflow, a step that Nelkap refuses to save throws one too, and the run fails unless you catch it. See [Errors](/reference/errors#nelkaperror). ## Crashes and outages Your app can stop in the middle of a run: a crash, a deploy, a host that cuts the function off. You write no code for it. Nelkap notices that the run stopped, and about five minutes later, it calls your app again: the run resumes from its last saved step. The same goes for a step the SDK couldn't save because Nelkap was out of reach. A step cut off before its result was saved runs again from its start. See [Failure recovery](/concepts/how-it-works#failure-recovery) for each case, and [Idempotency](/guides/idempotency) to make a second execution harmless. ## Time limits Nelkap puts no limit on how long a step or a run takes. A step can run as long as your app keeps it running, and a run can last months through its sleeps and waits. > [!WARNING] > > Keep each step shorter than your host's time limit on a request. When the host cuts a call off, the steps already saved are kept and the run resumes, but a single step longer than the limit is cut off every time and never ends. See [Deployment](/deploy/overview#time-limits). ## Replay Once you've fixed the cause, [replay](/concepts/runs#replay-a-run) the run from its page. The replay is a new run of the same workflow, with the same payload and tags, that runs every step from the start. The failed run stays as it was. # Idempotency Source: https://docs.nelkap.com/guides/idempotency Know when work can happen again, and make sure it leaves a single mark: one charge, one email, one row. A saved step never runs again. Yet the work inside a step can still happen twice, for example when a crash cuts the step off just after it charged a card, but before its result was saved. For work that leaves a mark outside your app, like a payment, an email, or a row in your database, make that second time harmless. This property is called idempotency, and this page shows the ways to get it. ## Duplicate execution | What happens | What runs again | | - | - | | A crash, a deploy, or a host's time limit cuts a step off before its result is saved. | That step, from its start, when the run resumes. | | The SDK can't reach Nelkap to save a step's result. | That step, when the run resumes about five minutes later. | | You [replay](/concepts/runs#replay-a-run) a run. | Every step, since a replay is a new run. | | An event is emitted twice, or a service calls your webhook twice. | The whole workflow, in a second run. | The first two cases only touch the step that was cut off: the steps saved before it return their saved results. ## Idempotency keys Many services accept a key with each request, and ignore a second request with a key they've already seen. Payment and email providers commonly do. Build the key from your data, so every attempt sends the same one: ```ts nelkap/workflows/cancel-order.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import Stripe from 'stripe'; import { nelkap } from '../client'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); type Order = { id: string; paymentIntentId: string; }; export async function cancelOrder(order: Order) { await nelkap.step.run('refund', () => stripe.refunds.create( { payment_intent: order.paymentIntentId }, { idempotencyKey: `refund_${order.id}` }, ), ); } ``` If the step runs again, Stripe sees the same key and returns the first refund instead of making a second one. > [!NOTE] > > A provider keeps a key for a limited time, at least 24 hours at [Stripe](https://docs.stripe.com/api/idempotent_requests). That covers a step that resumes after a crash, but not always a replay made days later. ## Upserts In your own database, give each row a key that says what it is, like a contact's email address, and insert or update in one statement: ```sql theme={"theme":{"light":"light-plus","dark":"dark-plus"}} INSERT INTO contacts (email, name) VALUES ($1, $2) ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name; ``` The first attempt adds the contact, and a second one updates the same row instead of adding another. ## Generated IDs When your code makes up an ID for something it creates, draw the ID in a step of its own, then create the thing with it: ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} const invoiceId = await nelkap.step.run( 'invoice id', () => crypto.randomUUID(), ); await nelkap.step.run('invoice', () => createInvoice({ id: invoiceId, orderId: order.id }), ); ``` The ID is saved, so a second attempt of the `invoice` step uses the same one. `createInvoice` can then insert with `ON CONFLICT (id) DO NOTHING`, or pass the ID as an idempotency key. ## Existence checks When a service takes no key, look for what a first attempt left before you act: ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} const ticket = await nelkap.step.run( 'ticket', async () => { const existingTicket = await findTicketByOrder( order.id, ); return existingTicket ?? createTicket(order.id); }, ); ``` A second attempt finds the ticket the first one created, and returns it. ## Runs and events in steps Code outside steps runs again on every call of the run. A `run` or an `emit` placed there would start a new run, or emit a new event, each time the run resumes. Place them in a step, so they happen once: ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} await nelkap.step.run('invoice run', () => nelkap.run(sendInvoice, order.id), ); await nelkap.step.run('paid event', () => nelkap.emit({ event: 'order.paid', payload: order.id, }), ); ``` ## Duplicate runs Some services send the same webhook twice, and your code may emit the same event twice. Each one starts its own run. When a run must happen once per order, per user, or per payment, let the first run claim it in your database, and let the others stop there. The [order fulfillment example](/examples/stripe-orders) shows how. > [!WARNING] > > A [replay](/concepts/runs#replay-a-run) runs every step again, including the ones that charged cards or sent emails. Before you replay a run, check that its steps with side effects are idempotent. # Loops and parallel steps Source: https://docs.nelkap.com/guides/loops Run steps in loops, side by side, and behind conditions, and split big jobs across several runs. Steps are ordinary function calls, so they work in loops, in conditions, and in `Promise.all`. On each call, the SDK matches them with their saved results in the order your code reaches them. This page shows how to write each case, and how to split jobs too big for a single run. ## Loops Each step in a loop is saved on its own. An import cut off halfway resumes at the first batch with no saved result: ```ts nelkap/workflows/import-contacts.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { importRows, splitIntoBatches } from '../../contacts'; import { nelkap } from '../client'; export async function importContacts(fileId: string) { const batches = await nelkap.step.run('batches', () => splitIntoBatches(fileId, 500), ); for (const [index, batchId] of batches.entries()) { await nelkap.step.run(`batch ${index}`, () => importRows(batchId), ); } } ``` The list of batches comes from a step too, so every call of the run loops over the same list. > [!TIP] > > Name each step after its item, like `batch 3`. A loop of `step.run('send')` works too, since steps with the same name are matched in order, but the run's page then can't tell you which step handled which item. ## Parallel steps Steps can run at the same time. Start them together and wait for all of them: ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} const [profile, orders] = await Promise.all([ nelkap.step.run('profile', () => getProfile(userId)), nelkap.step.run('orders', () => getOrders(userId)), ]); ``` The two functions run in parallel. Their results are saved in the order your code called the steps, so a run that resumes later matches them the same way. The same works for a list, with one step per item: ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} await Promise.all( userIds.map((userId) => nelkap.step.run(`digest ${userId}`, () => sendDigest(userId), ), ), ); ``` Each item's step counts toward the run's limit of 100 steps. For longer lists, see [Fan-out](#fan-out). ## Conditions A step can sit behind an `if`, as long as the condition takes the same path on every call. Base it on the payload or on the result of an earlier step: ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} const user = await nelkap.step.run('user', () => getUser(userId), ); if (user.plan === 'pro') { await nelkap.step.run('pro welcome', () => sendProWelcome(userId), ); } ``` > [!WARNING] > > Never base a condition on something read outside a step, like the time or a database. A run that resumes could take the other path, and its steps would no longer match the saved ones. See [Determinism](/concepts/steps#determinism). ## Step limit A run holds up to 100 steps, of every type, and 32 MB of saved steps in all. A run that goes over fails with `stepLimitReached`. Two ways keep big jobs under the limit: * Put several items in each step. A step that imports 500 rows counts once. * Split the job across runs, as below. ## Fan-out For a job of any size, give each part its own run. A first workflow lists the parts and starts a run for each one, then each run does its part: ```ts nelkap/workflows/import-file.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { importRows, splitIntoBatches } from '../../contacts'; import { nelkap } from '../client'; export async function importFile(fileId: string) { const batches = await nelkap.step.run('batches', () => splitIntoBatches(fileId, 500), ); for (const [index, batchId] of batches.entries()) { await nelkap.step.run(`start batch ${index}`, () => nelkap.run(importBatch, batchId), ); } } export async function importBatch(batchId: string) { await nelkap.step.run('import', () => importRows(batchId)); } ``` Starting each run in a step means a run that resumes later doesn't start the same batch twice. Give `importBatch` a [concurrency limit](/guides/concurrency) to set how many batches run at once. The first workflow still counts one step per batch. For more batches than one run can start, add a level: the first workflow starts a run per chunk of batches, and each of those starts a run per batch. The first run ends once it has started every batch, without waiting for them. To act when the last batch is done, count the batches done in your database, as the [large import example](/examples/large-import) does. # Sleep Source: https://docs.nelkap.com/guides/sleep Pause a run for minutes, days, or months, with nothing running in your app while it waits. A run can stop for a set time, then resume: a reminder three days after sign-up, a follow-up a week after an order, a new attempt an hour after a failure. While it sleeps, your app has nothing running and nothing in memory. Nelkap keeps the run, and calls your app again when the time is up. To pause until something happens rather than for a set time, see [Wait for an event](/guides/wait). ## Sleep for a duration Call `step.sleep` with a duration: ```ts nelkap/workflows/send-trial-reminder.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { sendTrialEndingEmail } from '../../emails'; import { nelkap } from '../client'; export async function sendTrialReminder(userId: string) { await nelkap.step.sleep('11 days'); await nelkap.step.run('reminder', () => sendTrialEndingEmail(userId), ); } ``` Started when a trial begins, this run sleeps for 11 days, then sends the reminder. The sleep ends the call: the code after it runs in a later call, once the 11 days are over. ## Durations A duration is a whole number and a unit, like `30 seconds` or `3 days`. The unit can be singular or plural. | Unit | Example | How it's counted | | - | - | - | | `second`, `minute`, `hour` | `90 seconds`, `2 hours` | As an exact length of time. | | `day`, `week` | `3 days`, `1 week` | As 24 hours and 7 days, whatever the daylight saving time. | | `month`, `year` | `6 months`, `1 year` | On the calendar, in UTC: `1 month` from January 15 ends on February 15. | A month from January 31 runs past the end of February and ends in early March. A sleep lasts up to a year. ## Wake-up time Nelkap checks sleeping runs every minute, so a run resumes within a minute after its time is up. A sleep of a few seconds therefore lasts up to a minute. When the run wakes, Nelkap calls your app, and your workflow runs from its first line: the steps before the sleep return their saved results, the sleep returns at once, and the work after it runs. > [!TIP] > > For a pause shorter than a minute, like a second between two requests to a rate-limited API, wait inside the step that makes the requests. The step holds the call open meanwhile, so keep such pauses short. ## Sleep until a date `step.sleep` takes a duration. To sleep until a date, work out the duration in a step, so every call of the run gets the same one: ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} export async function remindBeforeEvent(event: { id: string; startsAt: string; }) { const hours = await nelkap.step.run( 'hours left', () => { const hoursUntilStart = (Date.parse(event.startsAt) - Date.now()) / 3_600_000; return Math.max( 0, Math.floor(hoursUntilStart) - 24, ); }, ); await nelkap.step.sleep(`${hours} hours`); await nelkap.step.run('reminder', () => sendEventReminder(event.id), ); } ``` This run sends its reminder a day before the event. The number of hours is saved, so a later call doesn't compute a new one from a later time. If the event is less than a day away, the duration is `0 hours`, and the sleep returns at once, without pausing the run. ## Sleep names A sleep is saved like a step, named after its duration. When a run reaches two sleeps of `1 day`, the first matches the first saved one, and the second matches the second. > [!WARNING] > > Changing a sleep's duration in your code changes its name. A run that already slept reaches it on its next call as a new sleep, and pauses a second time. See [Versioning](/guides/versioning#changes-to-runs-in-progress). ## Errors | Error | When | | - | - | | `Error` | The duration isn't a whole number and a unit, like `1.5 days` or `tomorrow`. | | `NelkapError` `badRequest` | The sleep would last more than a year. | Like any error your workflow doesn't catch, either one fails the run. Every code is described in [Errors](/reference/errors#api-errors). ## Cancel a sleeping run While a run sleeps, you can cancel it from its page, for example when the user upgrades before the trial reminder goes out. See [Cancel a run](/concepts/runs#cancel-a-run). # Versioning Source: https://docs.nelkap.com/guides/versioning Deploy new code while runs are in progress, and know exactly what each run does with it. Runs can last days or months, so you'll often deploy a new version of a workflow while some of its runs are still in progress. Nelkap has no versions to manage: a run in progress executes your new code on its next call. This page explains what that means for each kind of change, so you can change a workflow without surprises. ## How runs match new code On each call, your workflow runs from its first line, and each step looks for its saved result by type and name, in the order your code reaches it. So what matters is the name of each step, not where it sits in your code: * A step that keeps its name returns its saved result, even if you moved it. * A step with a new name runs as a new step, even where an old one used to be. * A saved step your code no longer reaches is ignored. A run executes the new code from its next call: after a sleep or a wait, or when Nelkap calls again after a deploy cut a call off. ## Changes to runs in progress | Change | Effect on a run in progress | | - | - | | Change code after the point the run has reached. | Its next call runs the new code from there. | | Change the code inside a step the run has saved. | None: the saved result comes back. The new code runs only in runs that haven't reached the step. | | Move a step and keep its name. | None: the step is matched by its name. | | Add a step before the point the run has reached. | The step runs on the next call, since it has no saved result. | | Rename a step the run has saved. | Its work is done again, under the new name. | | Remove a step. | Its saved result is ignored. | | Change a sleep's duration, or a wait's key. | Once the run reaches it again, it pauses a second time, with the new duration or key. A run paused on it keeps its original wake-up time or key until it wakes. | | Remove one of several steps with the same name. | The steps after it shift: each one gets the saved result of the one before it. | Adding a step is often what you want, like an email that every run in progress should still send. When it isn't, branch on a version. > [!WARNING] > > Once runs exist, keep the names of their steps, the durations of their sleeps, and the keys of their waits as they are. Changing one makes the runs in progress do that work, or that pause, a second time. ## Version branching When a change must only apply to new runs, branch on a version saved in the run's first step. Add the step when you first write the workflow, returning `1`. When a change comes, return the next number and branch on it: ```ts nelkap/workflows/onboard-user.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { createProfile, sendWelcomeEmail, sendWelcomeSurvey, } from '../../activities'; import { nelkap } from '../client'; export async function onboardUser(userId: string) { const version = await nelkap.step.run('version', () => 2); await nelkap.step.run('profile', () => createProfile(userId)); // Runs from version 2 on send the survey. if (version >= 2) { await nelkap.step.run('survey', () => sendWelcomeSurvey(userId), ); } await nelkap.step.sleep('1 day'); await nelkap.step.run('welcome', () => sendWelcomeEmail(userId), ); } ``` Runs started earlier saved `1`, so they keep the old path. New runs save `2` and take the new one. ## Rename or remove a workflow A workflow's name is how Nelkap tells your app which code to run. Renaming a workflow counts as removing the old name and adding a new one. Removing a workflow cancels its runs that haven't started yet, and those sleeping or waiting. See [Add and remove workflows](/concepts/workflows#add-and-remove-workflows). To retire a workflow without cutting its runs short: **1. Stop new runs** Remove its events and its schedule, and stop starting it from your code. **2. Let the last runs end** Keep it registered, and filter your runs by the workflow until none is in progress. **3. Remove it** Remove it from `registerWorkflows`, and deploy. # Wait for an event Source: https://docs.nelkap.com/guides/wait Pause a run until something happens, like a person's answer or another system's callback, then resume with what it sent. Some work can't continue until something happens outside it: a manager approves a refund, a customer confirms an email address, another system finishes a job. A run can wait for that moment, for minutes or for months. While it waits, your app has nothing running. When the event arrives, the run resumes with the event's payload in hand. ## Wait on a key A run waits on a key that you choose. Your app wakes it by emitting an event with the same key: ![The refundOrder run has saved its review request step and is waiting on the key refund\_ord\_42. The refund.reviewed event, emitted with wakeOn refund\_ord\_42 and the payload approved true, wakes it.](https://mintcdn.com/holotus/zu12qw3F64kRzbLL/images/refund-approval-light.png?fit=max\&auto=format\&n=zu12qw3F64kRzbLL\&q=85\&s=0458d02a18d63b59df6ebdc60da0a066) ```ts nelkap/workflows/refund-order.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { askManagerToReview, refundPayment, sendRefundedEmail, } from '../../refunds'; import { nelkap } from '../client'; type Review = { approved: boolean; }; export async function refundOrder(orderId: string) { await nelkap.step.run('review request', () => askManagerToReview(orderId), ); const review = await nelkap.step.wait( `refund_${orderId}`, ); if (review.approved) { await nelkap.step.run('refund', () => refundPayment(orderId)); await nelkap.step.run('refunded email', () => sendRefundedEmail(orderId), ); } } ``` When the manager answers, your app emits the event, for example from the route that receives the answer: ```ts app/refunds.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { nelkap } from '../nelkap'; export async function reviewRefund( orderId: string, approved: boolean, ) { await nelkap.emit({ event: 'refund.reviewed', payload: { approved: approved }, wakeOn: `refund_${orderId}`, }); } ``` The run resumes, and `step.wait` returns the event's payload: here, the manager's decision. If the event has no payload, `step.wait` returns `null`. > [!NOTE] > > The type you give `step.wait`, here `Review`, isn't checked when the event arrives. It describes the payload your app emits, so keep the two in sync. ## Choosing a key Build the key from what the run waits for, like the order it's about. Your app can then emit the event from anywhere, without knowing the run's ID: | Key | The run waits for | | - | - | | `refund_${orderId}` | The review of a refund. | | `email_confirmed_${userId}` | A click on a confirmation link. | | `export_done_${exportId}` | The end of another system's job. | A key is up to 256 characters. The run's page shows the key it's waiting on, next to its status. ## Events sent before the wait > [!WARNING] > > An event wakes only the runs already waiting on its key when it arrives. An event emitted before the run reaches its wait isn't kept for it, and the run keeps waiting. So ask for what you wait for in the step just before the wait, like the email that asks the manager for a review. The run reaches its wait right after that step, long before anyone can answer. ## Shared keys An event wakes every run waiting on its key, not just one. That's useful to release many runs at once, like every order waiting for a warehouse to reopen. To wake a single run, make the key unique to it, for example with the ID of what it works on. ## Multiple waits A run can wait several times, on the same key or on different ones. Waits on the same key are matched in order, so a run that waits on `reply_${ticketId}` in a loop gets each reply in turn, as long as each one arrives while the run is waiting. ## Timeouts A run waits up to a year. After that, it fails with `waitExpired`. To give up sooner, start a second workflow that sleeps for the time you allow, then emits the event itself, with a payload that says the time ran out: ```ts nelkap/workflows/refund-order.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} export async function refundOrder(orderId: string) { await nelkap.step.run('review request', () => askManagerToReview(orderId), ); await nelkap.step.run('deadline', () => nelkap.run(expireReview, orderId), ); const review = await nelkap.step.wait( `refund_${orderId}`, ); if (review.approved) { await nelkap.step.run('refund', () => refundPayment(orderId)); } } export async function expireReview(orderId: string) { await nelkap.step.sleep('7 days'); await nelkap.step.run('expire', () => nelkap.emit({ event: 'refund.expired', payload: { approved: false }, wakeOn: `refund_${orderId}`, }), ); } ``` Register both workflows. If the manager answers within 7 days, the refund run has already resumed, and the late event wakes nothing. Otherwise, the run resumes with `approved: false`. ## Errors | Error | When | | - | - | | `NelkapError` `badRequest` | The key is empty, or longer than 256 characters. | Like any error your workflow doesn't catch, it fails the run. Every code is described in [Errors](/reference/errors#api-errors). ## Cancel a waiting run While a run waits, you can cancel it from its page, for example when the customer withdraws a request. See [Cancel a run](/concepts/runs#cancel-a-run). # FAQ Source: https://docs.nelkap.com/help/faq Short answers to the questions teams ask most about Nelkap. ## How Nelkap works > **Does my code run on Nelkap's servers?** > > No. Your workflows run in your own app, next to your database and your other code. Nelkap keeps each run's state, decides when it goes on, and calls your app's endpoint when there's work to do. See [How Nelkap works](/concepts/how-it-works). > **Do I need a queue, a worker, or a database for Nelkap?** > > No. Nelkap keeps the runs, the schedule, and the saved steps. Your app needs the SDK and one route. > **Which languages and runtimes does the SDK support?** > > The SDK is written in TypeScript, and works in JavaScript too. It needs Node.js 22 or later. See [Installation](/installation#requirements). > **Which frameworks and hosts work with Nelkap?** > > Any framework where a route can hand the SDK a standard web `Request`, on any host that serves it over public HTTPS: a server, a container, or a serverless function. The [framework guides](/frameworks/overview) cover Next.js, React Router, SvelteKit, Nuxt, Astro, Hono, and Express. > **Can I run workflows on my own computer?** > > Nelkap calls your app over the internet, so it can't reach `localhost`. Deploy to a staging environment, with its own project, and test there. See [Environments](/deploy/environments). ## Writing workflows > **Does Nelkap retry a failed step?** > > Your workflow decides. A failed step is saved like a result, so every later call sees the same failure, and your code chooses what comes next: take another path, or try again with a new step after a sleep. See [Error handling](/guides/failures#retries). > **Can a step run twice?** > > A saved step never runs again. A step cut off before its result was saved, by a crash or a deploy, runs again from its start. See [Idempotency](/guides/idempotency). > **How long can a run last?** > > As long as it needs. Nelkap puts no limit on a run or a step. A run can sleep or wait for up to a year at a time, as many times as its 100 steps allow. Your host may limit how long a single call lasts. See [Time limits](/deploy/overview#time-limits). > **Can one workflow start another?** > > Yes. Call `nelkap.run` inside a step, so the new run starts only once, even if the first one picks up again. See [Fan-out](/guides/loops#fan-out). > **Can I change a workflow while runs are in progress?** > > Yes. Runs in progress go on with your new code, from their last saved step. Some changes need care, like renaming a step that runs have already passed. See [Versioning](/guides/versioning). > **Where do my logs go?** > > Nelkap records each run and each step, with what it returned or the error it threw. What your code writes with `console.log` goes to your host's logs, as usual. Code outside steps runs again on every call of the run, so a line logged there can show up more than once. Log inside a step to write it once. ## Running in production > **What happens if my app is down?** > > Runs that are sleeping or waiting aren't affected. A run due while your app is down is tried three times, over about three minutes, then fails with `deliveryFailed`: replay it once your app is back. A run cut off in the middle picks up about five minutes later. See [Deployment](/deploy/overview#delivery-and-retries). > **What happens to runs when I deploy?** > > Nothing is lost. A run cut off by the deploy picks up from its last saved step, on your new version. See [Runs during a deploy](/deploy/overview#runs-during-a-deploy). > **How do I stop a workflow?** > > Cancel one run from its page. To stop a workflow for good, remove it from `registerWorkflows`: its runs that haven't started, and those sleeping or waiting, are canceled. See [Runs](/concepts/runs#cancel-a-run). > **How many runs can go at once?** > > Up to your plan's limit for the workspace: 50, 100, or 500 runs. Sleeping and waiting runs don't count. Runs over the limit wait their turn, and none is lost. See [Limits](/limits#by-plan). > **How long does Nelkap keep my runs?** > > 1, 7, or 15 days after a run ends, depending on your plan. Runs still going are kept for as long as they last. To keep them longer, [export them](/manage/exports). # Troubleshooting Source: https://docs.nelkap.com/help/troubleshooting What to check when a run doesn't start, doesn't reach your app, or doesn't go on. Most problems show on the run's page: its status, its error, and its last step tell you where it stopped. Find the symptom below, and the fix that goes with it. ## Endpoint ### Runs fail with `deliveryFailed` Nelkap called your endpoint three times, over about three minutes, without a `2xx` answer. Check, in this order: 1. The project's endpoint is the exact URL of your route, with `https://` and the full path. 2. Your app is deployed and running at that URL. 3. `NELKAP_SECRET` in this environment is the secret of this project. A staging secret in production, or an old secret after a rotation, gets a `401`. 4. Nothing answers before `handle`: a sign-in check, a middleware, a redirect, or your host's own protection, like Vercel's [Deployment Protection](/deploy/vercel#deployment-protection). 5. Your app registers the workflow. If it doesn't know the workflow, `handle` answers `404`. Once it's fixed, [replay](/concepts/runs#replay-a-run) the runs that failed. ### Check your endpoint from a terminal Send your endpoint a call with your secret: ```bash theme={"theme":{"light":"light-plus","dark":"dark-plus"}} curl -X POST https://your-app.com/api/nelkap \ -H "Authorization: Bearer $NELKAP_SECRET" \ -H "Content-Type: application/json" \ -d '{}' ``` The call names no workflow, so nothing runs. The answer tells you how far it got: | Answer | What it means | | - | - | | `404` with `{"error":"workflowNotFound"}` | Your route works, and the secret matches. | | `401` with `{"error":"secretInvalid"}` | The route works, but the secret doesn't match `NELKAP_SECRET`. | | Anything else | Something other than `handle` answered: check the URL, your middleware, and your host. | ## Starting runs ### `run` throws "Only a workflow passed to registerWorkflows can start a run." The function you passed to `run` isn't in the list given to `registerWorkflows` on this client. Add it to the list, and make sure the code that starts the run imports the same client as your route. See [Create your client](/installation#create-your-client). ### `run` throws `workflowNotFound` Nelkap has no enabled workflow with this name in the project of your API key. Check that: * `NELKAP_API_KEY` belongs to the project you're looking at, not to another environment. * The workflow is in the list every instance of your app registers. An instance that registers a list without it turns it off. ### `apiKeyMissing`, `apiKeyInvalid`, or `ipNotAllowed` * `apiKeyMissing`: `NELKAP_API_KEY` is empty in this environment. * `apiKeyInvalid`: the key was deleted, has expired, or was copied wrong. * `ipNotAllowed`: the key only takes requests from its allowed IPs. Serverless hosts often send requests from changing addresses: leave the list empty there, unless your host gives you fixed ones. See [API keys and secrets](/manage/api-keys). ### `throughputLimitReached` Your workspace sent more requests in one second than your plan allows. Try the call again a moment later. Inside a run, a step that gets this answer isn't lost: the run picks up again about five minutes later. See [Limits](/limits#by-plan). ### A run stays Pending A run goes to your app within a minute of being due. If it stays Pending longer, it's waiting its turn: its workflow, or your workspace, already has as many runs going as its limit allows. It starts as soon as one of them ends. See [Concurrency](/guides/concurrency). ## Running workflows ### "nelkap.step can only be called while Nelkap runs a workflow." A step was called outside a run. This happens when your code calls a workflow function directly, like `await onboardUser(id)`. Start it with `nelkap.run(onboardUser, id)` instead, and Nelkap runs it. ### A step ran twice A step cut off before its result was saved runs again when the run picks up, for example after a crash or a deploy. Make that harmless with an idempotency key, or check before you act. See [Idempotency](/guides/idempotency). ### A step's result has changed shape Results are saved as JSON: a `Date` comes back as a string, and `undefined` as `null`. Return plain values, and turn them back into what you need after the step. See [Steps](/concepts/steps#serialization). ### A run fails with `stepLimitReached` The run reached 100 steps, or 32 MB of saved steps. Put more work in each step, return less from each one, or split the job across runs. See [Loops and parallel steps](/guides/loops). ### A run stopped in the middle If a run shows Running but its app stopped, through a crash, a deploy, or a host time limit, Nelkap calls your app again about five minutes later, and the run picks up from its last saved step. You have nothing to do. See [Crashes and outages](/guides/failures#crashes-and-outages). ## Sleeps, waits, and schedules ### A run waits and never wakes An event wakes only the runs already waiting on its key when it arrives. Check that: * The run reached its `step.wait` before the event was emitted. Its page shows Waiting, with its key. * The event's `wakeOn` is exactly the key of the wait. * The event was emitted in the run's project. To wake it now, emit the event again with its key. See [Wait for an event](/guides/wait). ### A run sleeps a little longer than asked Nelkap wakes sleeping runs every minute, so a run goes on within a minute after its time. See [Sleep](/guides/sleep#wake-up-time). ### A schedule doesn't start at the time you expect * Schedules are in UTC. `0 9 * * *` is 9:00 UTC, not 9:00 where you are. * A new or changed schedule applies once your app has registered it. On a serverless host, your app starts with the first request it gets after a deploy: start a run or emit an event from the new deploy to send your list right away. See [Workflow registration](/deploy/overview#workflow-registration). See [Schedules](/triggers/schedules). # What is Nelkap? Source: https://docs.nelkap.com/index Nelkap runs your workflows and AI agents to completion, through crashes, deploys, and long pauses. Nelkap is a durable execution platform for TypeScript. You write a workflow as an ordinary async function and split it into steps. Nelkap saves the result of each step as it finishes. If your server crashes or you deploy in the middle of a run, the run picks up from its last saved step instead of starting over, and the steps already saved never run again. > **[Quickstart](/quickstart)** > > Create your account and run your first workflow in about ten minutes. > **[Frameworks](/frameworks/overview)** > > Add Nelkap to Next.js, React Router, SvelteKit, Nuxt, Astro, Hono, or Express. > **[How Nelkap works](/concepts/how-it-works)** > > See what happens during a run, and how it gets through crashes and deploys. > **[Build with AI](/build-with-ai)** > > Connect Claude Code, Cursor, or VS Code to these docs, so the code they write follows them. ## Why durable execution Some work doesn't fit in a single request. An onboarding series sends emails days apart. A refund waits for a manager's approval. An AI agent calls a model a dozen times, with tools in between. Without help, that means a queue, scheduled jobs, a table to remember where each task stands, and code to pick up what stopped halfway. With Nelkap, that work is one function that reads from top to bottom: ```ts onboardUser.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { Nelkap } from '@nelkap/sdk'; import { createProfile, sendWelcomeEmail } from './activities'; const nelkap = new Nelkap(); export async function onboardUser(userId: string) { await nelkap.step.run('profile', () => createProfile(userId)); await nelkap.step.sleep('1 day'); await nelkap.step.run('welcome', () => sendWelcomeEmail(userId), ); } ``` This workflow creates a profile, waits a day, then sends a welcome email. * Nelkap saves the profile as soon as it's created. If your app restarts afterwards, the run goes on with the welcome email, and the profile isn't created a second time. * During the day of waiting, nothing runs and no server is kept busy. Nelkap calls your app again when the day is over. * Every run is recorded, step by step, so you can see what each one did and why it stopped. ## Where your code runs Nelkap doesn't host your code. When a run has work to do, Nelkap calls a route in your app, and the SDK runs the workflow right there, next to your database and your dependencies. You keep your framework, your host, and the way you deploy. See [How Nelkap works](/concepts/how-it-works). ## Use cases > **[AI agents](/examples/ai-agent)** > > Save each model answer and each tool call. A restart never asks the model the same question twice. > **[Human in the loop](/examples/refund-approval)** > > Pause a run until someone approves, for minutes or for weeks, then carry on with their answer. > **[Emails over time](/examples/welcome-emails)** > > Send a series of emails days apart, from one function that reads top to bottom. > **[Payments and orders](/examples/stripe-orders)** > > Ship each paid order exactly once, even when the payment arrives twice. > **[Scheduled jobs](/examples/weekly-report)** > > Start a workflow every hour, every night, or every Monday morning. > **[Large jobs](/examples/large-import)** > > Split a big import into batches that run a few at a time and never start over. > **[Background tasks](/triggers/run)** > > Answer the request at once, and let the heavy work finish in the background. > **[Webhooks and events](/triggers/events)** > > Start workflows from your own events or from calls sent by other services. # Installation Source: https://docs.nelkap.com/installation Install the SDK, give it your keys, and organize your workflows as your app grows. The SDK is one package, `@nelkap/sdk`. It runs inside your app: it starts runs, answers Nelkap's calls, and runs your workflows step by step. ## Requirements * Node.js 22 or later. The SDK comes with its TypeScript types. * An app that Nelkap can reach over HTTPS once deployed, since Nelkap calls your app to run each workflow. ## Install the SDK ```bash npm theme={"theme":{"light":"light-plus","dark":"dark-plus"}} npm install @nelkap/sdk ``` ```bash pnpm theme={"theme":{"light":"light-plus","dark":"dark-plus"}} pnpm add @nelkap/sdk ``` ```bash yarn theme={"theme":{"light":"light-plus","dark":"dark-plus"}} yarn add @nelkap/sdk ``` ## Add your keys The SDK needs two values from your project. It reads them from your environment: | Variable | What it's for | Where to find it | | - | - | - | | `NELKAP_API_KEY` | Lets your app start runs, emit events, and save steps. | Create one under API keys, for this project. | | `NELKAP_SECRET` | Lets your app check that each call really comes from Nelkap. | Reveal it from your project, under Projects. | Keep both on the server only. If you store them elsewhere, for example in a secrets manager, pass them to the client as [options](/reference/nelkap#options) instead. ## Create your client Create one client for your whole app, and import it wherever you need it: ```ts nelkap.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { Nelkap } from '@nelkap/sdk'; export const nelkap = new Nelkap(); ``` One client matters because it holds the list of workflows you register. The route that answers Nelkap and the code that starts runs both look up your workflows in that list, so they must use the same client. ## Organize your workflows A single file is enough to start. As your app grows, give each workflow its own file, and register them all in one place: ```ts nelkap/client.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { Nelkap } from '@nelkap/sdk'; export const nelkap = new Nelkap(); ``` ```ts nelkap/workflows/onboard-user.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { createProfile } from '../../activities'; import { nelkap } from '../client'; export async function onboardUser(userId: string) { await nelkap.step.run('profile', () => createProfile(userId)); } ``` ```ts nelkap/index.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { nelkap } from './client'; import { onboardUser } from './workflows/onboard-user'; import { sendWeeklyReport } from './workflows/send-weekly-report'; nelkap.registerWorkflows([ { name: 'onboardUser', handler: onboardUser }, { name: 'sendWeeklyReport', handler: sendWeeklyReport, cron: '0 9 * * 1', }, ]); export { nelkap, onboardUser, sendWeeklyReport }; ``` Import from `nelkap/index.ts` in your route and wherever you start runs. That way, your workflows are always registered before they're needed. > [!NOTE] > > `registerWorkflows` takes every workflow of the project at once. A workflow you leave out is turned off. See [Workflows](/concepts/workflows#add-and-remove-workflows). # Limits Source: https://docs.nelkap.com/limits The limits Nelkap applies to workflows, runs, steps, events, and workspaces, and what happens when you reach one. Limits keep every project fast and predictable. Some are the same on every plan. Others grow with your plan. When a call goes over a limit, Nelkap turns it down and the SDK throws a [`NelkapError`](/reference/errors) that names the limit. A run that goes over its limit of steps fails with `stepLimitReached`, and the reason shows on its page. ## On every plan | What | Limit | | - | - | | Workflow name | 256 characters | | Events a workflow listens for | 100, each up to 256 characters | | Concurrency of a workflow | 1 to 500 | | Steps in a run | 100, of every kind | | Saved steps in a run | 32 MB in all | | Step name | 128 characters | | Step error | 2,048 characters, the rest is cut | | Run error | 2,048 characters, the rest is cut | | Sleep | 1 year | | Wait | 1 year | | Wait key | 256 characters | | Event name | 256 characters | | Tags | 10 per run or event, each up to 128 characters | | JSON nesting | 64 levels | | API keys | 10 per project | | Allowed IPs | 20 per API key or webhook | | Workspaces you own | 5 | ## By plan | What | Starter | Pro | Business | | - | - | - | - | | Projects per workspace | 10 | 50 | 100 | | Workflows per project | 100 | 500 | 1,000 | | Runs going at once in the workspace | 50 | 100 | 500 | | Requests per second | 50 | 500 | 1,000 | | Request size | 1 MB | 2 MB | 3 MB | | Run, step, and event history | 1 day | 7 days | 15 days | | Webhooks per workspace | 50 | 100 | 500 | | Alerts per workspace | 10 | 100 | 500 | | Audit logs | — | — | Included | * Runs going at once counts the runs your app is running. Sleeping and waiting runs don't count. Runs over the limit wait their turn, and none is lost. * Requests per second counts every request to Nelkap: starting a run, emitting an event, saving a step, the signal a running run sends each minute, and each call to a webhook. * Request size applies to each payload, each event, and each step's result. * History is how long an ended run stays in your project, with its steps and events. * Projects, webhooks, and alerts count across the whole workspace. Creating one more than your plan allows is turned down in the console. See all plans on the [pricing page](https://nelkap.com/pricing). # Account Source: https://docs.nelkap.com/manage/account Sign in, keep your profile up to date, and see where your account is signed in. Your account belongs to you, not to a workspace: you keep the same one in every workspace you join. It holds your profile, your workspaces, and your sessions. ## Sign in Nelkap has no password to remember or to leak. To sign in, enter your email address, and Nelkap emails you a code. Enter the code, and you're in. * The code works for 10 minutes, and once. * Asking for a new code replaces the last one. * After three tries, wait up to 10 minutes before you try again. Signing in for the first time creates your account. Nelkap writes its emails in the language of the page you first signed in on, English or French. A session lasts 7 days on a device. After that, you sign in again. ## Profile Your profile, under General in your account settings, holds your first and last name, your occupation, your country, and your time zone. The console shows every date in your time zone, which is UTC until you choose yours. Your email address can't be changed, since it's how you sign in. ## Workspaces Workspaces, in your account settings, lists every workspace you belong to, with your role in each. From there, you can: * Create a workspace, which you'll own. See [Workspaces and projects](/manage/workspaces). * Accept or reject an invitation. See [Members and roles](/manage/members#accept-an-invitation). * Leave a workspace. See [Members and roles](/manage/members#leave-a-workspace). ## Sessions Sessions, in your account settings, lists every device where you're signed in, with its location and when its session ends. End any session but the one you're using, for example on a lost laptop: that device has to sign in again. To end the one you're using, sign out. # API keys and secrets Source: https://docs.nelkap.com/manage/api-keys The two values your app uses to talk with Nelkap, and how to limit, rotate, and revoke them. Your app and Nelkap prove who they are to each other with two values. Each one works in a single direction: | Value | Who sends it | What it proves | Where your app reads it | | - | - | - | - | | API key | Your app, on every request to Nelkap | The request comes from your app, for this project. | `NELKAP_API_KEY` | | Project secret | Nelkap, on every call to your endpoint | The call comes from Nelkap. | `NELKAP_SECRET` | Keep both on the server, in your host's environment variables or a secrets manager. Never put them in code that runs in a browser. ## API keys An API key lets your app start runs, emit events, register workflows, and save steps, all in one project. A project can have up to 10 API keys, so each app or each server can have its own. ### Create a key Go to API keys and create one. A key takes: * A name, up to 256 characters, that says where it's used, like `production servers`. * A project. Its endpoint must be set first, since a key is what lets your app start runs. * Allowed IPs, if you want: up to 20 addresses or ranges, like `203.0.113.7` or `203.0.113.0/24`, in IPv4 or IPv6. Requests from any other address are turned down with `ipNotAllowed`. Leave it empty to accept requests from anywhere. * An expiry date, if you want. After that date, the key is turned down with `apiKeyInvalid`. You can reveal a key's value at any time, to copy it into your app's environment. ### Change or revoke a key Edit a key to change its name, its allowed IPs, or its expiry date. The change applies to the next request. Delete a key to revoke it. Requests using it are turned down at once with `apiKeyInvalid`, so put the new key in place first. ### Key rotation 1. Create a new key in the same project. 2. Set it as `NELKAP_API_KEY` in your app, and deploy. 3. Once every instance of your app runs with the new key, delete the old one. Runs in progress aren't affected: each request uses the key your app has at that moment. ## Project secret Every project has one secret. Nelkap sends it in the `Authorization` header of each call to your endpoint, and `handle` compares it with `NELKAP_SECRET`. A call without the right secret gets a `401`, and no workflow runs. You can reveal the secret from your project at any time. ### Rotate the secret Rotate the secret if it leaked. The old one stops working at once, and your endpoint turns every call away until it has the new one: 1. Rotate the secret from your project, and copy the new one. 2. Set it as `NELKAP_SECRET` in your app, and deploy right away. Calls turned away in the meantime are tried again, up to three attempts in about three minutes. A run whose attempts all fail shows `deliveryFailed`: [replay it](/concepts/runs#replay-a-run) once the new secret is live. ## Permissions | Action | Role | | - | - | | See API keys and projects | Reader and above | | Create, change, reveal, and delete API keys | Member and above | | Reveal a project's secret | Member and above | | Rotate a project's secret | Admin and owner | See [Members and roles](/manage/members). # Audit logs Source: https://docs.nelkap.com/manage/audit-logs See who changed what in your workspace, and when. When a key is deleted or a run is canceled, you want to know who did it. Audit logs record every change made in the workspace from the console, with the person who made it and the time. Audit logs come with the Business plan. Admins and the owner can read them. ## Recorded actions | Target | Changes recorded | | - | - | | Workspace | Created, changed | | Membership | Invitation sent, accepted, or rejected. Role changed. Member removed. Member left. | | Project | Created, changed, secret rotated | | API key | Created, changed, deleted | | Webhook | Created, changed, secret rotated | | Alert | Created, changed, deleted | | Run | Replayed, canceled | | Export | Requested | Each entry shows the person, the action (create, update, or delete), the target, and what happened, like `Deleted API key`. What your app does through the API, like starting runs or emitting events, isn't in the audit logs: it shows in your [runs](/concepts/runs) and [events](/triggers/events#event-history). ## Search The latest changes come first. Filter them by person, by action, by target, or by period, from the last hour to the last 30 days. ## Retention Entries are kept for 15 days, like the history of your runs on the Business plan. # Exports Source: https://docs.nelkap.com/manage/exports Download your workspace's workflows, runs, steps, and events as CSV files. Nelkap keeps your runs for as long as your plan's history: 1, 7, or 15 days. An export gives you a copy to keep, to analyze in a spreadsheet, or to load into your own database. Admins and the owner can request exports. ## Request an export Go to Exports and choose what to export: | Choice | Files in the archive | | - | - | | Workflows | `workflows.csv` | | Runs and their steps | `runs.csv` and `steps.csv` | | Events | `events.csv` | An export covers every project of the workspace, with everything Nelkap still keeps. Nelkap builds the archive in the background. Its status goes from pending to processing, then to ready. A workspace builds one export at a time: request the next one once the last is ready. ## Download Once the export is ready, download it from the list. It's a zip archive, available for 7 days. ## File format Each file starts with a row of column names, then one row per workflow, run, step, or event. The columns cover everything Nelkap keeps: IDs, names, statuses, payloads, results, errors, and dates. Rows link to each other by ID, like a step to its run, and a run to its workflow and its event. * Dates are in UTC, in ISO 8601, like `2026-10-24T09:30:00.000Z`. * Payloads, results, scores, and tags are written as JSON. * A value that a spreadsheet could take for a formula is escaped, so the files open safely in Excel or Google Sheets. ## Long-term history To keep your runs longer than your plan does, export them at least as often as your history lasts: every day with 1 day of history, every week with 7 days. A run still going when you export it shows with its status at that moment, and again in the next export. Each row keeps its ID from one export to the next, so merge them by ID. ## Failed exports An export that can't be built shows as failed. Request it again. # Members and roles Source: https://docs.nelkap.com/manage/members Invite your team to a workspace, and give each person the access they need. Everyone who works on your workflows can have their own access to the workspace. Each member has a role, and the role decides what they can see and change. ## Roles A workspace has four roles. Each one can do everything the role below it can: | Role | What it adds | | - | - | | Reader | Sees the projects, workflows, runs, events, webhooks, API keys, alerts, metrics, and scores. | | Member | Creates and changes projects, API keys, webhooks, and alerts. Reveals secrets and keys. Replays and cancels runs. | | Admin | Invites and manages members. Rotates secrets. Reads the [audit logs](/manage/audit-logs), and [exports](/manage/exports) the workspace's history. | | Owner | Renames the workspace and chooses its plan. Gives the Admin role, and hands the workspace over. | A workspace has a single owner: the person who created it, until they hand it over. Give each person the lowest role that lets them do their work. A Reader is enough to follow runs and read errors, for example for someone on support. ## Invite a member Go to Members, and invite the person with their email address and a role. Admins and the owner see this page. * The person needs a Nelkap account first. If they don't have one yet, ask them to [sign in](https://nelkap.com/auth) once, with the address you'll invite. * An admin can invite readers and members. Only the owner can invite an admin. Nelkap emails the person, in their own language, to tell them they're invited. The invitation shows as pending until they answer it. ## Accept an invitation Your invitations are listed under Workspaces, in your account settings. Accept one to join the workspace, or reject it. Once you accept, the workspace shows in your list, and you have access with the role you were invited with. If you reject an invitation, the workspace's admins see it as rejected, and can't invite you again until they delete it. ## Change a member's role Edit a member to give them another role. You can change a pending invitation too, before the person answers it. * An admin can change the role of readers and members, and give them the Reader or Member role. * The owner can change the role of anyone else, and give any role. ## Transfer ownership To make someone else the owner, the owner gives them the Owner role. The person must have accepted their invitation. The former owner becomes an admin, so the workspace keeps a single owner. ## Remove a member Delete a member to take away their access at once. Deleting a pending invitation cancels it. An admin can remove readers and members. The owner can remove anyone else. ## Leave a workspace To leave a workspace yourself, go to Workspaces in your account settings, and leave it. The owner can't leave: hand the workspace over first. # Workspaces and projects Source: https://docs.nelkap.com/manage/workspaces How your team, your apps, and your environments fit into workspaces and projects. Nelkap organizes everything in two levels. A workspace is your team: its members, its plan, and its limits. A project is one app in one environment: its endpoint, its keys, and its workflows, runs, and events. ## Workspaces A workspace holds your projects and the people who work on them. Everything the plan decides applies to the whole workspace: how many runs go at once, how many requests per second it takes, and how long history is kept. You create a workspace from your account, with a name, a plan, and the name of its first project. You become its owner. One person can own up to 5 workspaces, and be a member of as many as they're invited to. Rename a workspace from its settings. Only the owner can. ## Projects A project is what your app connects to. It has: * An endpoint: the URL Nelkap calls to run your workflows. * A secret, which your app uses to check that each call comes from Nelkap. * API keys, which your app uses to start runs, emit events, and save steps. * Its workflows, runs, events, webhooks, and alerts. Two projects never see each other's workflows, runs, or events. A run started with a project's API key runs on that project's endpoint, and an event emitted in one project starts and wakes runs of that project only. ## Create a project Go to Projects and create one, with a name, up to 100 characters, and its endpoint. Creating and changing projects takes the Member role or above. A new workspace's first project has no endpoint yet: edit it to set one before you create its API keys. Your plan sets how many projects your workspace can have. See [Limits](/limits#by-plan). ## Project structure A project has a single endpoint, so all its workflows run in one app. Create one project: * Per app. Two apps with their own workflows each need their own project, since each registers its own list of workflows. * Per environment. Staging and production each get a project, so tests never touch production. See [Environments](/deploy/environments). ## Edit a project Edit a project to rename it or change its endpoint. A new endpoint applies to the next call Nelkap makes: runs in progress go on at the new address. From the project, you can also reveal its secret and rotate it. Rotating takes the Admin role. See [API keys and secrets](/manage/api-keys). ## Project filter Lists of workflows, runs, events, webhooks, API keys, and alerts show the whole workspace, with a column for the project. Choose a project at the top of the page to see only that project, on every page you open next. # Alerts Source: https://docs.nelkap.com/monitor/alerts Get an email when a project's failed runs reach a number you choose within an hour. You can't watch your runs all day. An alert watches them for you: when a project's failed runs reach a number you set, within an hour, Nelkap emails your workspace's owner and admins with what failed and where. ## Create an alert Go to Alerts and create one. An alert takes: * A name, up to 100 characters, that says what it watches, like `Checkout failures`. * A project. An alert watches every workflow of that project. * A limit: the number of failed runs within an hour that sends the email, from 1 to 10,000,000. * A status: enabled, or disabled to pause it without deleting it. A limit of 1 sends an email for any failure in the hour. A higher limit ignores the odd failure and warns you when failures pile up. ## Alert conditions Nelkap checks alerts every hour, on the hour. For each enabled alert, it counts the project's runs that failed since the alert's last email, going back an hour at most. If the count reaches the limit, it sends the email. So an alert emails at most once an hour, and never counts the same failure twice. Each alert shows when it last sent an email. ## Recipients The email goes to the workspace's owner and admins. Members and readers don't get it. To add someone, give them the Admin role. See [Members and roles](/manage/members). The email names each alert that reached its limit, with its project and the number of runs that failed. Open the failed runs to see their errors: filter your runs by the project, the Failed status, and the last hour. See [Runs](/concepts/runs#find-a-run). ## Edit or delete an alert Edit an alert to change its name, project, limit, or status. Delete it when you no longer need it. Creating, changing, and deleting alerts takes the Member role or above. Your plan sets how many alerts your workspace can have. See [Limits](/limits#by-plan). # Metrics Source: https://docs.nelkap.com/monitor/metrics Watch the health of your runs over time: how many start, wait, run, and fail, and how long they take. Metrics chart your runs over time, so you see a problem build up before it hits your customers: a backlog that keeps growing, runs that take longer each day, an error rate that climbs after a deploy. They're computed from your runs, with nothing to set up in your code. ## Filters * Period: the last hour, 24 hours, 3 days, 7 days, 15 days, or 30 days. The last hour is charted minute by minute, the last 24 hours hour by hour, and longer periods day by day. * Project: one project, or the whole workspace, chosen at the top of the page. Metrics are updated every minute. Times are shown in your own time zone. ## Charts | Chart | What it shows | What to watch for | | - | - | - | | Backlog | Pending runs, waiting for their turn, and ready runs, on their way to your app. | A backlog that keeps growing: runs come in faster than they're let through. | | Concurrency | Runs your app is running. | A flat line at your limit: runs are waiting for a turn. | | Suspended | Sleeping and waiting runs. | Waiting runs piling up: the events that should wake them may not come. | | Outcomes | Runs that completed, failed, or were canceled, by the time they ended. | A rise in failed runs. | | Triggers | Runs started from your code, by events, by schedules, and by replays. | A drop: something stopped starting runs. | | Latency | How long runs take from creation to end, and how long they wait before they start, at the median and the 95th percentile. | Queue wait climbing: runs wait longer for a turn. | | Error rate | Failed runs, out of those that completed or failed. | A jump right after a deploy. | The median is the time half of the runs stay under. The 95th percentile is the time 95 runs out of 100 stay under, so it shows the slowest runs without being thrown off by a single one. ## Latency A run's time from creation to end includes its sleeps and waits. A workflow that sleeps a day takes at least a day, even if its steps take a second. So compare latency with itself over time, rather than with a fixed number. Queue wait is the time between a run's creation and its start. It rises when runs wait for a turn under a [concurrency limit](/guides/concurrency). Since Nelkap starts pending runs every minute, a queue wait of up to a minute is normal. # Scores Source: https://docs.nelkap.com/monitor/scores Measure how good each run was, like the quality of an AI answer, and watch it over time. A run can complete and still do a poor job: an AI agent answers, but off the point, or a support flow ends with a human taking over. Status alone can't tell you that. A score can. Record a number or a yes or no from your workflow, and Nelkap charts it over time, so you see right away whether a change made your runs better or worse. ## Record a score Call `step.score` with a name and a value: ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} export async function answerTicket(ticketId: string) { const reply = await nelkap.step.run('reply', () => draftReply(ticketId), ); await nelkap.step.score( 'resolved without a human', reply.resolved, ); await nelkap.step.score( 'confidence', reply.confidence, ); } ``` * A number, like a relevance from 0 to 1 or a rating from 1 to 5, is charted as an average. * A yes or no, like whether a ticket was solved, is charted as a pass rate: the share of runs that recorded `true`. The name is up to 128 characters. Runs that record the same name share a chart, so use the same name across workflows to compare them. A score is saved like a step. When the run picks up later, the score is already recorded and isn't counted twice. ## Score sources Score what you can measure at that point of the run: * A value your own code computes, like whether an order shipped on time. * A judgment from a model, like the relevance of an answer graded by a second model call, made in a step so it's saved. * A person's feedback, received with [`step.wait`](/guides/wait), like a thumbs up on a support reply. ## Charts Each score name, with its kind of value, gets its own chart, for the period and project you choose: from the last hour, minute by minute, to the last 30 days, day by day. A drop the day after a deploy points to that change. Each score also shows on its run's page, next to the step that recorded it. # Quickstart Source: https://docs.nelkap.com/quickstart Create your account, write your first workflow, and follow your first run, in about ten minutes. In this guide, you'll add a workflow to your app that creates a new user's profile, waits a day, and sends them a welcome email. You'll start a run and follow it step by step in your project. You need a Node.js 22 app that you can deploy to a public HTTPS address, because Nelkap calls your app over the internet. This guide works with any framework. For a guide written for yours, see [Frameworks](/frameworks/overview). **1. Create your account** [Sign up](https://nelkap.com/auth) and start your free trial. Your workspace comes with a first project: it holds your workflows, their runs, and the keys your app uses. **2. Set the endpoint** Your endpoint is the URL in your app that Nelkap calls to run your workflows. Edit your project and set its endpoint to the address the route will have once deployed, like `https://your-app.com/api/nelkap`. You'll add that route in a moment. **3. Get your keys** Your app needs two values from your project: * An API key, which lets your app start runs and save steps. Create one under API keys, for this project. * Your project's secret, which lets your app check that each call really comes from Nelkap. Reveal it from your project, under Projects. Add both to your app's environment: ```bash .env theme={"theme":{"light":"light-plus","dark":"dark-plus"}} NELKAP_API_KEY=your-api-key NELKAP_SECRET=your-project-secret ``` The SDK reads them from `process.env`. **4. Install the SDK** ```bash npm theme={"theme":{"light":"light-plus","dark":"dark-plus"}} npm install @nelkap/sdk ``` ```bash pnpm theme={"theme":{"light":"light-plus","dark":"dark-plus"}} pnpm add @nelkap/sdk ``` ```bash yarn theme={"theme":{"light":"light-plus","dark":"dark-plus"}} yarn add @nelkap/sdk ``` **5. Write a workflow** Create a `nelkap.ts` file with the Nelkap client and your workflow: ```ts nelkap.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { Nelkap } from '@nelkap/sdk'; import { createProfile, sendWelcomeEmail, } from './activities'; export const nelkap = new Nelkap(); export async function onboardUser(userId: string) { await nelkap.step.run('profile', () => createProfile(userId), ); await nelkap.step.sleep('1 day'); await nelkap.step.run('welcome', () => sendWelcomeEmail(userId), ); } nelkap.registerWorkflows([ { name: 'onboardUser', handler: onboardUser }, ]); ``` Replace `createProfile` and `sendWelcomeEmail` with your own functions. Here is what each part does: * `onboardUser` is your workflow: an async function that takes the run's payload, here a user ID. * `step.run` runs a piece of work and saves what it returns. When the run picks up later, the step gives back its saved result instead of running again. * `step.sleep` pauses the run for a day. Nothing runs in your app during that day. * `registerWorkflows` tells Nelkap which workflows your app runs, as soon as the file is loaded. **6. Add the route** Add a POST route at the path of your endpoint, and pass each request to `nelkap.handle`: ```ts Next.js theme={"theme":{"light":"light-plus","dark":"dark-plus"}} // app/api/nelkap/route.ts import { after } from 'next/server'; import { nelkap } from '@/nelkap'; export async function POST(request: Request) { return nelkap.handle(request, { waitUntil: (promise) => after(() => promise), }); } ``` ```ts Any framework theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { nelkap } from './nelkap'; export const handleNelkap = async ( request: Request, ): Promise => { return nelkap.handle(request); }; ``` `handle` takes a standard web `Request`. It checks the secret, answers Nelkap at once, and runs the workflow in the background. For the exact route in your framework, see [Frameworks](/frameworks/overview). **7. Deploy** Deploy your app with its two environment variables, so Nelkap can reach the route at your endpoint. See [Deployment](/deploy/overview) for what the endpoint needs. **8. Start a run** Start a run from your server code, for example right after a user signs up: ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { nelkap, onboardUser } from './nelkap'; await nelkap.run(onboardUser, 'user_42'); ``` The second argument is the run's payload: Nelkap passes it to `onboardUser`. `run` returns as soon as Nelkap has the run, without waiting for the workflow to finish. **9. View the run** Open your project's runs and select the new run. The profile step shows what it returned, and the run is sleeping. A day later, Nelkap calls your app again, the welcome email goes out, and the run completes. To watch the whole run now, set the sleep to `1 minute` while you try it out. # emit Source: https://docs.nelkap.com/reference/emit Emit an event to start workflows and wake waiting runs. Emits an event when something happens in your app, like a payment or a sign-up. An event does two things: * It starts a run of every workflow listening for it, with the event's payload. * With `wakeOn`, it also wakes every run waiting on that key, and their `step.wait` returns the event's payload. The code that emits the event doesn't need to know which workflows listen, or which runs wait. ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} nelkap.emit( input: EmitEventInput, ): Promise<{ runIds: string[] }> ``` ## Parameters **event** `string` *required* The event's name, like `invoice.paid`. Every enabled workflow of the project that lists it in its `events` starts a run. Up to 256 characters. **payload** `unknown` Any JSON value. The runs the event starts get it as their payload, and the runs it wakes get it from `step.wait`. **tags** `string[]` Tags given to the event and to the runs it starts. Up to 10, each up to 128 characters. **wakeOn** `string` The key of the runs to wake. Every run of the project waiting on this key when the event arrives goes on. Up to 256 characters. ## Returns **runIds** `string[]` The IDs of the runs the event started, or an empty list if no workflow listens for it. ## Errors | Error | When | | - | - | | `NelkapError` `badRequest` | A field breaks a rule above. | Any request to Nelkap can also get one of the errors listed in [API errors](/reference/errors#api-errors), like `throughputLimitReached`. ## Example Start every workflow listening for an event: ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} await nelkap.emit({ event: 'user.signed_up', payload: { id: user.id, email: user.email }, }); ``` Wake the runs waiting on a key, here the refund of order `ord_42`: ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} await nelkap.emit({ event: 'refund.reviewed', payload: { approved: true }, wakeOn: 'refund_ord_42', }); ``` ## See also * [Events](/triggers/events): start workflows with events, and wake waiting runs. * [Webhooks](/triggers/webhooks): turn calls from other services into events. # Errors Source: https://docs.nelkap.com/reference/errors Every error the SDK throws, and every reason a run can fail. The SDK throws three kinds of errors, so you can tell at once where a problem comes from: | Error | Where it comes from | | - | - | | [`NelkapError`](#nelkaperror) | Nelkap turned a request down, like a run of a workflow it doesn't know. | | [`StepError`](#steperror) | The work inside a step failed, like a declined card. | | [`Error`](#sdk-errors) | The SDK was used the wrong way, like a step called outside a run. | ## NelkapError Thrown when Nelkap turns a request down: by `run` and `emit`, and by a step that Nelkap refuses to save. Its `message` is the reason's code, so you can check for a given reason. **status** `number` The HTTP status of Nelkap's answer. **message** `string` The reason's code, like `workflowNotFound`. See [API errors](#api-errors). **issues** `\{ path: PropertyKey[]; message: string \}[]` For `badRequest`, the fields at fault and what is wrong with each. ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { NelkapError } from '@nelkap/sdk'; try { await nelkap.run(onboardUser, 'user_42'); } catch (error) { if ( error instanceof NelkapError && error.message === 'throughputLimitReached' ) { // Too many requests this second: try again soon. } } ``` ## StepError Thrown by [`step.run`](/reference/step-run) when its function threw. The failure is saved like a result, so the step throws the same `StepError` each time the run reaches it, without running the function again. Catch it to take another path. See [Error handling](/guides/failures#step-errors). **message** `string` The message of the error the function threw, up to 2,048 characters. **cause** `unknown` The error the function threw, on the call where the step ran. Later calls only have the saved message. ## API errors The codes a `NelkapError` can carry: | Code | Status | When | | - | - | - | | `badRequest` | 400 | A value breaks a rule. `issues` lists which ones. | | `apiKeyMissing` | 401 | The API key is empty. | | `apiKeyInvalid` | 401 | The API key is wrong, deleted, or expired, or none is set. | | `workspaceExpired` | 402 | The workspace is no longer active. | | `workflowLimitReached` | 402 | You registered more workflows than your plan allows per project. | | `stepLimitReached` | 402 | The run reached its limit of steps. | | `ipNotAllowed` | 403 | The API key takes requests only from its allowed IPs. | | `workflowNotFound` | 404 | The project has no enabled workflow with this name. | | `runNotFound` | 404 | The run isn't running anymore, so the step wasn't saved. | | `payloadLimitReached` | 413 | The request is larger than your plan allows. | | `throughputLimitReached` | 429 | Too many requests in the same second for your plan. | | `unexpected` | 500 | Something went wrong on Nelkap's side. | When saving a step gets `throughputLimitReached` or `unexpected`, or can't reach Nelkap at all, the SDK doesn't throw. The run stops where it is, and Nelkap picks it up again about five minutes later. See [Crashes and outages](/guides/failures#crashes-and-outages). ## Run failures A run fails when your workflow throws an error it doesn't catch, and the run's page shows its message. A run can also fail for a reason of Nelkap's own: | Reason | Shown as | | - | - | | `deliveryFailed` | Your endpoint could not be reached after three attempts. | | `stepLimitReached` | This run reached its maximum number or size of steps. | | `waitExpired` | This run waited more than a year for its event. | ## SDK errors The SDK throws a plain `Error` when it's used the wrong way: | Message | When | | - | - | | `Only a workflow passed to registerWorkflows can start a run.` | [`run`](/reference/run) got a function that isn't registered. | | `nelkap.step can only be called while Nelkap runs a workflow.` | A step was called outside a run, for example from a route. | | `Invalid duration: …` | [`step.sleep`](/reference/step-sleep) got a duration it can't read. | ## See also * [Error handling](/guides/failures): what to do when a step or a run fails. * [Troubleshooting](/help/troubleshooting): what to check for each error. # handle Source: https://docs.nelkap.com/reference/handle Answer the calls Nelkap makes to your endpoint. Answers the calls Nelkap makes to your project's endpoint. Call it from the POST route at that address, and return what it returns. On each call, `handle` checks that the request carries your project's secret, finds the workflow Nelkap asks for among the ones you registered, and answers at once. The workflow then runs in the background, until it completes, fails, or pauses. ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} nelkap.handle( request: Request, options?: { waitUntil?: (promise: Promise) => void; }, ): Promise ``` ## Parameters **request** `Request` *required* The POST request Nelkap sent to your endpoint, as a standard web [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request). **options.waitUntil** `(promise: Promise\) =\> void` Your host's `waitUntil`, for serverless hosts that stop all work once the response is sent, like Vercel. `handle` passes it the run, so the host keeps it going after the answer. Not needed on a server that keeps running, like a Node.js server or a container. See [Serverless hosts](/frameworks/overview#serverless-hosts). ## Returns A standard web `Response`, to return from your route as is: | Status | Body | When | | - | - | - | | 202 | `{}` | The call is accepted, and the workflow runs in the background. | | 401 | `{ "error": "secretInvalid" }` | The request doesn't carry your project's secret, or the client has no secret set. | | 404 | `{ "error": "workflowNotFound" }` | No workflow of this name was registered on this client. | ## Example A Next.js route handler, which keeps the run going after the answer with `after`: ```ts app/api/nelkap/route.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { after } from 'next/server'; import { nelkap } from '@/lib/nelkap'; export async function POST(request: Request) { return nelkap.handle(request, { waitUntil: (promise) => after(() => promise), }); } ``` ## See also * [Frameworks](/frameworks/overview): the route in every framework. * [Deployment](/deploy/overview): what your endpoint needs, and how Nelkap calls it. * [Next.js](/frameworks/nextjs): a complete setup, from the keys to the first run. # Nelkap Source: https://docs.nelkap.com/reference/nelkap The client your workflows, your route, and your code use to reach Nelkap. The client is the one object of the SDK. Your code uses it to start runs and emit events, your route uses it to answer Nelkap, and your workflows use it for their steps. Create one client for your whole app, and import it wherever you need it. It holds the workflows you register, and that's how the route knows which function to run when Nelkap calls. ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} new Nelkap(options?: NelkapOptions) ``` ## Options Every option is optional. Without options, the client reads its values from your environment. **apiKey** `string` An API key of your project. The client sends it with every request to Nelkap: registering workflows, starting runs, emitting events, and saving steps. Defaults to `NELKAP_API_KEY`. **secret** `string` Your project's secret. Nelkap sends it with every call to your endpoint, and [`handle`](/reference/handle) turns down any call that doesn't carry it. Defaults to `NELKAP_SECRET`. **apiUrl** `string` The address of the Nelkap API. Defaults to `NELKAP_API_URL`, or to `https://api.nelkap.com` when it isn't set. ## Methods | Method | What it does | | - | - | | [`registerWorkflows`](/reference/register-workflows) | Sets your project's workflows and sends them to Nelkap. | | [`run`](/reference/run) | Starts a run of a workflow. | | [`emit`](/reference/emit) | Emits an event, to start workflows and wake waiting runs. | | [`handle`](/reference/handle) | Answers the calls Nelkap makes to your endpoint. | | [`step`](/reference/step-run) | The steps your workflows use: [`run`](/reference/step-run), [`sleep`](/reference/step-sleep), [`wait`](/reference/step-wait), and [`score`](/reference/step-score). | ## Example With your keys in `NELKAP_API_KEY` and `NELKAP_SECRET`, the client needs no options: ```ts nelkap.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { Nelkap } from '@nelkap/sdk'; export const nelkap = new Nelkap(); ``` If your keys come from somewhere else, like a secrets manager or variables of another name, pass them as options: ```ts nelkap.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { Nelkap } from '@nelkap/sdk'; export const nelkap = new Nelkap({ apiKey: process.env.MY_NELKAP_API_KEY, secret: process.env.MY_NELKAP_SECRET, }); ``` ## Exports The package exports the client, its two error classes, and the types you may need in your own code: ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { Nelkap, NelkapError, StepError, } from '@nelkap/sdk'; import type { EmitEventInput, NelkapOptions, Workflow, WorkflowHandler, } from '@nelkap/sdk'; ``` ## See also * [Installation](/installation): add your keys and organize your workflows. * [Errors](/reference/errors): what `NelkapError` and `StepError` carry. # registerWorkflows Source: https://docs.nelkap.com/reference/register-workflows Set your project's workflows and send them to Nelkap. Tells the client which workflows your app runs, and tells Nelkap what starts each one. Call it once, when your app starts, with every workflow of your project. The list you pass becomes your project's workflows: new ones are added, the ones already there take their new settings, and the ones you leave out are turned off. ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} nelkap.registerWorkflows(workflows: Workflow[]): void ``` ## Parameters **workflows** `Workflow[]` *required* Every workflow of your project. See [Add and remove workflows](/concepts/workflows#add-and-remove-workflows). **Workflow** **name** `string` *required* The name Nelkap knows the workflow by, unique within the list. Nelkap uses it to tell your app which workflow to run, so keep it stable: renaming a workflow turns off the old name. Up to 256 characters. **handler** `WorkflowHandler` *required* The workflow function. It takes the run's payload as its only argument. **events** `string[]` The events that start a run of the workflow each time one is emitted, by your app or by a webhook. Up to 100, each up to 256 characters. **cron** `string` A schedule that starts a run at each time it matches, in five fields and in UTC. `0 9 * * 1` starts one every Monday at 9:00 UTC. **concurrency** `number` The most runs of the workflow that may go at once, from 1 to 500. The others wait their turn, in order. Without it, the workflow's runs only count toward your workspace's limit. ## Behavior * The list is sent to Nelkap right away, in the background. `run` and `emit` wait until it has arrived, so they never reach a workflow Nelkap doesn't know yet. * If the list can't be sent, the next `run` or `emit` sends it again, and throws if that fails too. * Calling `registerWorkflows` again replaces the whole list. * Every workflow in the list can also be started from your code with [`run`](/reference/run), whatever else starts it. ## Errors `registerWorkflows` itself never throws. If Nelkap turns the list down, the next `run` or `emit` throws a [`NelkapError`](/reference/errors#nelkaperror): | Code | When | | - | - | | `badRequest` | The list breaks a rule above, like two workflows with the same name or a schedule that isn't valid. | | `workflowLimitReached` | The list has more workflows than your plan allows per project. | ## Example ```ts nelkap/index.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { nelkap } from './client'; import { importContacts } from './workflows/import-contacts'; import { onboardUser } from './workflows/onboard-user'; import { sendWeeklyReport } from './workflows/send-weekly-report'; nelkap.registerWorkflows([ { name: 'onboardUser', handler: onboardUser, events: ['user.created'], }, { name: 'sendWeeklyReport', handler: sendWeeklyReport, cron: '0 9 * * 1', }, { name: 'importContacts', handler: importContacts, concurrency: 5, }, ]); ``` Here, `onboardUser` starts each time `user.created` is emitted, `sendWeeklyReport` every Monday morning, and `importContacts` from your code, with at most five imports at once. ## See also * [Workflows](/concepts/workflows): what happens to the workflows you add, change, or leave out. * [Events](/triggers/events) and [Schedules](/triggers/schedules): what starts a workflow. * [Concurrency](/guides/concurrency): how `concurrency` works. # run Source: https://docs.nelkap.com/reference/run Start a run of a workflow from your code. Starts a run of a workflow with the payload you give it, for example when a user signs up or places an order. Use it when your code knows exactly which workflow should run. To let any number of workflows react to something that happened, [emit an event](/reference/emit) instead. `run` returns as soon as Nelkap has recorded the run, without waiting for the workflow to finish. You can call it from a request handler and answer right away, while the run goes on. ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} nelkap.run( workflow: T, payload?: Parameters[0], options?: { tags?: string[] }, ): Promise<{ runId: string }> ``` ## Parameters **workflow** `WorkflowHandler` *required* The workflow function itself, as passed to [`registerWorkflows`](/reference/register-workflows). You pass the function rather than its name, so TypeScript checks the payload against the workflow's argument. **payload** `Parameters\[0]` The value the workflow gets as its argument. Any JSON value, like a user ID or an order. **options.tags** `string[]` Tags to find the run by in your project, like a customer or a plan. Up to 10, each up to 128 characters. ## Returns **runId** `string` The ID of the new run. ## Errors | Error | When | | - | - | | `Error` | The function wasn't passed to `registerWorkflows`. | | `NelkapError` `workflowNotFound` | The project has no enabled workflow with this name. | | `NelkapError` `badRequest` | The payload or a tag breaks a rule above. | Any request to Nelkap can also get one of the errors listed in [API errors](/reference/errors#api-errors), like `throughputLimitReached`. ## Example ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { nelkap, onboardUser } from './nelkap'; export async function signUp(email: string) { const user = await createUser(email); const { runId } = await nelkap.run( onboardUser, user.id, { tags: ['beta'] }, ); return { user: user, runId: runId }; } ``` The user is created, and their onboarding starts in the background. `signUp` returns right away, with the run's ID. ## See also * [Triggering](/triggers/run): payloads, tags, and every other way to start a run. # step.run Source: https://docs.nelkap.com/reference/step-run Run a piece of work, and save what it returns. Runs a piece of work and saves what it returns. Once the step is saved, the work is never done again in that run: each later call gets the saved result back at once. Use it for any work you don't want done twice, and for anything a later step needs: a call to another service, a write to your database, or a model call. ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} nelkap.step.run( name: string, fn: () => T | Promise, ): Promise ``` ## Parameters **name** `string` *required* The step's name, shown on the run's page, up to 128 characters. Nelkap finds the step again by its name. A name can repeat, like in a loop: steps of the same name are matched in the order your code reaches them. **fn** `() =\> T | Promise\` *required* The work to do. What it returns must fit in JSON. ## Returns What `fn` returned, passed through JSON. A `Date` comes back as a string, and `undefined` as `null`, on the first call as on every later one. ## Errors | Error | When | | - | - | | `StepError` | `fn` threw. The error is saved, and the step throws it again each time the run reaches it, without running `fn` again. | | `NelkapError` `badRequest` | The name breaks a rule above. | | `NelkapError` `stepLimitReached` | The run has reached its limit of steps. | | `NelkapError` `payloadLimitReached` | What `fn` returned is larger than your plan allows. | | `Error` | It was called outside a run. | ## Example ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} export async function shipOrder(orderId: string) { const label = await nelkap.step.run('label', () => createShippingLabel(orderId), ); await nelkap.step.run('tracking email', () => sendTrackingEmail(orderId, label.trackingNumber), ); } ``` If your app restarts once the label is created, the run picks up with the email, using the saved label. No second label is created. ## See also * [Steps](/concepts/steps): what belongs in a step, how steps are matched, and when one can run twice. * [Error handling](/guides/failures): catch a failed step and take another path. # step.score Source: https://docs.nelkap.com/reference/step-score Record a score of the run, charted over time. Records how good a run was, like the relevance of an AI answer or whether a ticket was solved without help. Each score name gets its own chart in your project, so you can see whether a change to your code makes runs better or worse. ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} nelkap.step.score( name: string, value: number | boolean, ): Promise ``` ## Parameters **name** `string` *required* The score's name, up to 128 characters. Runs that record the same name share a chart. **value** `number | boolean` *required* A number, charted as an average, or a yes or no, charted as a pass rate. ## Behavior The score is saved like a step. Each later call skips it, so a run records each score once, however many times it picks up. ## Errors | Error | When | | - | - | | `NelkapError` `badRequest` | The name breaks a rule above. | | `NelkapError` `stepLimitReached` | The run has reached its limit of steps. | | `Error` | It was called outside a run. | ## Example ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} export async function answerTicket(ticketId: string) { const reply = await nelkap.step.run('reply', () => draftReply(ticketId), ); await nelkap.step.score('resolved', reply.resolved); await nelkap.step.score( 'confidence', reply.confidence, ); } ``` Here, `resolved` is charted as the share of tickets solved, and `confidence` as the average confidence of the replies. ## See also * [Scores](/monitor/scores): how scores are charted, and how to read them. # step.sleep Source: https://docs.nelkap.com/reference/step-sleep Pause the run for a duration. Pauses the run for a set time, like a reminder three days later. The call ends, and nothing runs in your app while the run sleeps. Once the time is up, Nelkap calls your app again, and the run goes on with the next line. ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} nelkap.step.sleep(duration: string): Promise ``` ## Parameters **duration** `string` *required* A count and a unit, like `30 minutes` or `1 day`. The unit is second, minute, hour, day, week, month, or year, with or without an s. Up to a year. Months and years follow the calendar, so `1 month` from January 15 ends on February 15. ## Behavior * The sleep is saved like a step, with its duration as its name. Two sleeps of `1 day` in the same run are matched in order. * A duration of zero, like `0 hours`, goes on at once, without pausing the run. * While the run sleeps, you can cancel it from its page. ## Errors | Error | When | | - | - | | `Error` | The duration isn't a count and a unit, or it was called outside a run. | | `NelkapError` `badRequest` | The duration is longer than a year. | ## Example ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} export async function sendTrialReminder(userId: string) { await nelkap.step.sleep('12 days'); await nelkap.step.run('reminder', () => sendTrialEndingEmail(userId), ); } ``` Started when a trial begins, the run sleeps for 12 days, then sends the reminder. ## See also * [Sleep](/guides/sleep): durations, calendar months, and how to sleep until a date. # step.wait Source: https://docs.nelkap.com/reference/step-wait Pause the run until an event arrives with its key. Pauses the run until something happens outside it, like a person's approval or another system's callback. The call ends, and nothing runs in your app while the run waits. When an event arrives with the same key, Nelkap calls your app again, and `step.wait` returns that event's payload. ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} nelkap.step.wait(wakeOn: string): Promise ``` ## Parameters **wakeOn** `string` *required* The key to wait on, up to 256 characters. Build it from what you wait for, like `refund_${orderId}`, so the code that emits the event can rebuild it without knowing the run's ID. The wait is saved with this key as its name, cut to 128 characters. ## Returns The payload of the event that woke the run, or `null` if it had none. `T` sets its type, without checking it. ## Behavior * An event [emitted](/reference/emit) with the same `wakeOn` wakes the run. It wakes every run waiting on the key, not just one. * An event only wakes the runs already waiting when it arrives. So trigger what you wait for, like the email to a manager, in a step before the wait. * A run can wait up to a year. After that, it fails with `waitExpired`. * While the run waits, you can cancel it from its page. ## Errors | Error | When | | - | - | | `NelkapError` `badRequest` | The key breaks a rule above. | | `Error` | It was called outside a run. | ## Example ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} export async function refundOrder(orderId: string) { await nelkap.step.run('review request', () => askManagerToReview(orderId), ); const review = await nelkap.step.wait<{ approved: boolean; }>(`refund_${orderId}`); if (review.approved) { await nelkap.step.run('refund', () => refundPayment(orderId), ); } } ``` When the manager answers, wake the run from your app with an event that has the same key: ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} await nelkap.emit({ event: 'refund.reviewed', payload: { approved: true }, wakeOn: `refund_${orderId}`, }); ``` ## See also * [Wait for an event](/guides/wait): choose the key, and make sure the run waits first. * [Refund approval](/examples/refund-approval): the complete flow, with the route that receives the answer. * [emit](/reference/emit): every field of an event. # Events Source: https://docs.nelkap.com/triggers/events Emit events from your app to start every workflow that listens for them, and to wake the runs waiting on their key. An event records that something happened in your app, like `user.created` or `invoice.paid`. You emit it once, and Nelkap does two things with it: * It starts a run of every workflow that listens for the event. * It wakes every run waiting on the event's key, if the event has one. The code that emits the event doesn't need to know what happens next. To react to a sign-up in a new way, you add a workflow that listens for `user.created`, without touching the sign-up code. ![The app emits user.created with the payload user\_42. Nelkap starts a run of onboardUser and a run of addToNewsletter, both with the payload user\_42.](https://mintcdn.com/holotus/wXODVU55G3H-UZVw/images/event-fanout-light.png?fit=max\&auto=format\&n=wXODVU55G3H-UZVw\&q=85\&s=cb624fae4b70ba809d76d13e29999a49) ## Listen for an event List the events a workflow listens for when you register it: ```ts nelkap/index.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} nelkap.registerWorkflows([ { name: 'onboardUser', handler: onboardUser, events: ['user.created'], }, { name: 'addToNewsletter', handler: addToNewsletter, events: ['user.created'], }, ]); ``` A workflow can listen for up to 100 events. Each run it starts receives the event's payload as its own payload, so the workflow's argument has the payload's type: ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} export async function addToNewsletter(userId: string) { await nelkap.step.run('subscribe', () => subscribeToNewsletter(userId), ); } ``` ## Emit an event Call `nelkap.emit` from your server code: ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} const { runIds } = await nelkap.emit({ event: 'user.created', payload: 'user_42', }); ``` Here, one event starts two runs, one of `onboardUser` and one of `addToNewsletter`, both with the payload `user_42`. `emit` returns the IDs of the runs it started, or an empty list if no workflow listens for the event. | Field | Required | What it sets | | - | - | - | | `event` | Yes | The event's name, up to 256 characters. | | `payload` | No | Any JSON value, passed to every run the event starts, and returned by every wait it wakes. | | `tags` | No | Up to 10 labels, each up to 128 characters, given to every run the event starts. | | `wakeOn` | No | The key of the waiting runs to wake, up to 256 characters. | > [!TIP] > > Name events after what happened, in the past tense, like `order.shipped`. A name that says what happened, rather than which workflow should run, lets the list of listeners grow without changing the code that emits it. ## Wake waiting runs A run can pause until an event arrives with a given key, with [`step.wait`](/guides/wait). To wake it, emit an event with `wakeOn` set to that key: ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} await nelkap.emit({ event: 'refund.reviewed', payload: { approved: true }, wakeOn: 'refund_ord_42', }); ``` Every run waiting on `refund_ord_42` resumes, and its `step.wait` returns the event's payload. The same event also starts the workflows that listen for `refund.reviewed`, like any event. The IDs `emit` returns are those of the runs it started, not of the runs it woke. > [!WARNING] > > An event wakes only the runs already waiting when it arrives. An event emitted before the run reaches its wait doesn't wake it later. See [Events sent before the wait](/guides/wait#events-sent-before-the-wait). ## Emit from a workflow A workflow can emit events too, for example to let other workflows know that an order shipped. Emit inside a step, so the event goes out once, even when the run resumes later: ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} await nelkap.step.run('shipped event', () => nelkap.emit({ event: 'order.shipped', payload: orderId, }), ); ``` ## Events from other services Services like a payment provider can't call `nelkap.emit`, but they can call a URL. A webhook gives you that URL, and turns each call into an event. See [Webhooks](/triggers/webhooks). ## Event history The Events page of the console lists every event, whether your app emitted it or a webhook received it, with its project, the time it arrived, and the number of runs it started. Filter events by name, by project, by period, or by whether they started any run, for example to find events no workflow listened for. Events are kept for the history of your plan, like runs. See [Retention](/concepts/runs#retention). ## Errors | Error | When | | - | - | | `NelkapError` `badRequest` | A field breaks a limit above. `issues` says which. | | `NelkapError` `payloadLimitReached` | The payload is larger than your plan allows. | | `NelkapError` `throughputLimitReached` | Your workspace made more requests this second than your plan allows. | Every code is described in [Errors](/reference/errors#api-errors). # Triggering Source: https://docs.nelkap.com/triggers/run The four ways to start a run, and how to start one from your code with nelkap.run. A run starts in one of four ways. A workflow can use several of them at once, and every workflow can be started from your code, whatever else starts it. | Trigger | Use it when | How | | - | - | - | | Your code | Your code knows exactly which workflow to start, like onboarding right after a sign-up. | `nelkap.run`, on this page. | | [Events](/triggers/events) | Something happened, and any number of workflows may react to it. | `nelkap.emit`, and `events` on each workflow. | | [Schedules](/triggers/schedules) | The work repeats at fixed times, like a weekly report. | `cron` on the workflow. | | [Webhooks](/triggers/webhooks) | Another service, like a payment provider, calls a URL when something happens. | A webhook created in the console. | ## Start a run Call `nelkap.run` with the workflow function and its payload: ```ts app/signup.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { nelkap, onboardUser } from '../nelkap'; import { createUser } from './users'; export async function signUp(email: string) { const user = await createUser(email); const { runId } = await nelkap.run(onboardUser, user.id); return { user: user, runId: runId }; } ``` * You pass the function itself, not its name. The SDK finds the name it was registered under, and TypeScript checks the payload against the workflow's argument. * The function must be one you passed to [`registerWorkflows`](/concepts/workflows#register-workflows). Any other function throws an error. * `run` returns as soon as Nelkap has recorded the run, with its ID. It doesn't wait for the workflow to finish, so a request handler can answer right away. The run then starts within a minute. See [Lifecycle](/concepts/runs#lifecycle). ## Payload The payload is the argument your workflow receives. Its type comes from the workflow's signature: ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} type Order = { id: string; address: string; }; export async function shipOrder(order: Order) { await nelkap.step.run('label', () => createLabel(order.address), ); } await nelkap.run(shipOrder, { id: 'ord_42', address: '1 Main St, Springfield', }); ``` It can be any JSON value: a string, a number, an object, or an array. A workflow that takes no argument is started without a payload: `nelkap.run(sendDigest)`. See [Payload](/concepts/runs#payload) for its size limit and how it's sent. ## Tags Tags label the run, so you can filter runs by them in the console, for example to see every run of a customer: ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} await nelkap.run(onboardUser, 'user_42', { tags: ['team_7', 'beta'], }); ``` A run takes up to 10 tags, each up to 128 characters. ## Start a run from a workflow A workflow can start runs of other workflows, for example one for each item of a list. Start them inside a step: ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} await nelkap.step.run('invoice run', () => nelkap.run(sendInvoice, orderId), ); ``` > [!WARNING] > > Outside a step, `nelkap.run` runs again on every call of the workflow, and starts a new run each time. Inside a step, the run starts once, and the step's saved result keeps its ID. The two runs are independent: the first continues without waiting for the second. See [Fan-out](/guides/loops#fan-out) for jobs split across many runs. ## Many runs at once Each call to `nelkap.run` is one request to Nelkap, and your plan sets how many requests your workspace can make per second: 50, 500, or 1,000. Above that, `run` throws a `NelkapError` with the code `throughputLimitReached`. To start thousands of runs, don't loop over `nelkap.run` in a request handler. Start one run that lists the items and starts a run per item, each in its own step, as in [Fan-out](/guides/loops#fan-out). The steps run one after the other, and a start turned down for `throughputLimitReached` fails only its own step: catch it, sleep, and try again, as in [Retries](/guides/failures#retries). ## Errors | Error | When | | - | - | | `Error` | The function wasn't passed to `registerWorkflows`. | | `NelkapError` `workflowNotFound` | The project has no enabled workflow with this name. It may not be registered yet in this environment. | | `NelkapError` `badRequest` | The payload or a tag breaks a limit. `issues` says which. | | `NelkapError` `payloadLimitReached` | The payload is larger than your plan allows. | | `NelkapError` `throughputLimitReached` | Your workspace made more requests this second than your plan allows. | Every code is described in [Errors](/reference/errors#api-errors). # Schedules Source: https://docs.nelkap.com/triggers/schedules Start a workflow at fixed times, like every hour or every Monday morning, with a cron schedule. Some work repeats at fixed times: a report every Monday, a cleanup every night, a sync every hour. Give the workflow a schedule, and Nelkap starts a run each time the schedule comes due. Nothing runs in your app in between, and you don't need a scheduler of your own. ## Add a schedule Set `cron` when you register the workflow: ```ts nelkap/index.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} nelkap.registerWorkflows([ { name: 'sendWeeklyReport', handler: sendWeeklyReport, cron: '0 9 * * 1', }, ]); ``` This workflow starts a run every Monday at 9:00 UTC. A workflow has one schedule at most, and can listen for events and be started from your code too. ## Cron syntax A schedule is a cron expression with five fields, separated by spaces: | Field | Values | | - | - | | Minute | 0 to 59 | | Hour | 0 to 23 | | Day of the month | 1 to 31 | | Month | 1 to 12, or `JAN` to `DEC` | | Day of the week | 0 to 7, where 0 and 7 are Sunday, or `SUN` to `SAT` | In each field, `*` means every value, `1-5` a range, `1,15` a list, and `*/15` every fifteenth value. | Schedule | Starts a run | | - | - | | `* * * * *` | Every minute. | | `*/15 * * * *` | Every 15 minutes. | | `0 * * * *` | Every hour, on the hour. | | `0 2 * * *` | Every day at 2:00 UTC. | | `0 9 * * 1-5` | Every weekday at 9:00 UTC. | | `0 9 * * MON` | Every Monday at 9:00 UTC. | | `0 0 1 * *` | On the first day of each month, at midnight UTC. | | `0 6 1 1,4,7,10 *` | On the first day of each quarter, at 6:00 UTC. | > [!NOTE] > > Schedules run in UTC, all year round. A schedule set for 9:00 in Paris, `0 8 * * *` in winter, runs at 10:00 in Paris once summer time starts. ## Payload A scheduled run has no payload: the workflow receives `null`, so it takes no argument. When the work depends on the time, read the time in a step, so every call of the run gets the same value: ```ts nelkap/workflows/send-weekly-report.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { buildReport, emailReport, startOfLastWeek, } from '../../reports'; import { nelkap } from '../client'; export async function sendWeeklyReport() { const weekStart = await nelkap.step.run('week start', () => startOfLastWeek(new Date()).toISOString(), ); const report = await nelkap.step.run('report', () => buildReport(weekStart), ); await nelkap.step.run('email', () => emailReport(report.id)); } ``` ## Timing * After you add a schedule, the first run starts at the next time it matches, never right away. * Nelkap checks schedules every minute, so a run starts within a minute of its time. * If runs couldn't start for a while, the times missed meanwhile give a single run, not one per missed time. * Scheduled runs go through the same [concurrency limits](/guides/concurrency) as any other run. > [!TIP] > > When a run must end before the next one starts, like a sync that takes longer than its interval, give the workflow `concurrency: 1`. The next run then waits its turn instead of overlapping. ## Change or remove a schedule Change `cron` and deploy: the next run follows the new schedule as soon as your app registers its workflows. Remove `cron`, or the whole workflow, to stop the runs. > [!WARNING] > > On a serverless host, your app registers its workflows on its first request after a deploy, not when the deploy ends. A new or changed schedule waits for that request. See [Workflow registration](/deploy/overview#workflow-registration). ## Test a schedule To test a scheduled workflow without waiting for its time, start a run from your code. It runs the same workflow, with no payload: ```ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} await nelkap.run(sendWeeklyReport); ``` Its trigger shows as API rather than Cron. For a complete scheduled workflow, see the [weekly report example](/examples/weekly-report). # Webhooks Source: https://docs.nelkap.com/triggers/webhooks Give another service a URL to call, and turn each of its calls into an event that starts your workflows. Many services announce what happens on their side by calling a URL you give them: a payment succeeded, a pull request was merged, a form was submitted. A webhook is that URL. Nelkap receives each call, turns it into an event in your project, and starts every workflow that listens for that event, with the call's body as the payload. You don't write, host, or scale any code to receive the calls. ## Create a webhook **1. Create the webhook** Go to Webhooks in the console and create one. Choose its project and the event it emits, like `checkout.completed`. **2. Copy its URL** Reveal the webhook's URL and copy it. It looks like `https://api.nelkap.com/webhooks/`. **3. Give the URL to the other service** Paste the URL in the other service's webhook settings, as the address it calls. **4. Listen for the event** Register a workflow that listens for the webhook's event: ```ts nelkap/index.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} nelkap.registerWorkflows([ { name: 'fulfillOrder', handler: fulfillOrder, events: ['checkout.completed'], }, ]); ``` Each call now starts a run of `fulfillOrder`. Its payload is the call's JSON body, exactly as the service sent it, so type it after the service's own documentation: ```ts nelkap/workflows/fulfill-order.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}} import { createShipment, sendConfirmation } from '../../orders'; import { nelkap } from '../client'; type Checkout = { id: string; customer_email: string; }; export async function fulfillOrder(checkout: Checkout) { await nelkap.step.run('shipment', () => createShipment(checkout.id), ); await nelkap.step.run('confirmation', () => sendConfirmation(checkout.customer_email), ); } ``` ## Test a webhook Send a call yourself, with the body the service would send: ```bash theme={"theme":{"light":"light-plus","dark":"dark-plus"}} curl -X POST https://api.nelkap.com/webhooks/ \ -H 'Content-Type: application/json' \ -d '{"id":"cs_42","customer_email":"ada@example.com"}' ``` The event shows on the Events page, with the runs it started. ## Accepted requests Each call must be a POST with a JSON body, up to your plan's request size of 1 to 3 MB. Nelkap answers: | Status | When | | - | - | | `200` | The event was emitted, and the workflows that listen for it started their runs. | | `400` | The body isn't JSON, or it's nested more than 64 levels deep. | | `402` | The workspace is no longer active. | | `403` | The call came from an IP address the webhook doesn't allow. | | `404` | No enabled webhook has this URL. | | `413` | The body is larger than your plan allows. | | `429` | Your workspace made more requests this second than your plan allows. | Most services call again later when they don't get a `2xx` answer. > [!NOTE] > > A webhook emits its event without tags and without a `wakeOn` key, so it starts runs but doesn't wake waiting ones. To wake a run from another service's call, receive the call in your app and [emit the event](/triggers/events#wake-waiting-runs) yourself. ## Security The secret in the URL is what proves a call is meant for your project. Treat the URL like a password: * If it leaks, rotate the webhook's secret. The old URL stops working at once, so paste the new one in the other service right after. * To accept calls only from the service's servers, set the webhook's allowed IPs: up to 20 addresses or ranges, like `203.0.113.7` or `203.0.113.0/24`, in IPv4 or IPv6. Nelkap doesn't check the signatures some services add to their calls. When you need that check, receive the call in a route of your app, check the signature there, then emit the event with [`nelkap.emit`](/triggers/events#emit-an-event). The [order fulfillment example](/examples/stripe-orders) does this for Stripe. ## Disable a webhook Disable a webhook to stop it for a while without losing its URL. While it's disabled, its calls get a `404` and emit nothing. Enable it again, and the same URL works as before. You can also change the event a webhook emits, or its allowed IPs, at any time. ## Webhook list The Webhooks page of the console lists each webhook with its event, its project, its allowed IPs, its status, and the number of events it has emitted. The events themselves are listed with every other event, with the runs each one started. See [Event history](/triggers/events#event-history). ## Permissions | Action | Role | | - | - | | See webhooks | Every member of the workspace | | Create, change, disable a webhook, or reveal its URL | Member or above | | Rotate a webhook's secret | Admin or above | Your plan sets how many webhooks your workspace can have: 50, 100, or 500. See [Limits](/limits).