Vibecode Lindy
track this build4 phases, 12 steps, beginner friendly0%The visible AI assistants + automation loop is buildable, but a credible replacement needs more than the first screen. Lindy earns its keep through connectors, auth, reliability, so expect a weekend or multi-day build and a narrower personal scope.
You are building a lean indie version of Lindy. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Lindy · indie build A single-workflow automation runner whose hard problems are surviving at-least-once delivery without repeating a side effect, keeping connector credentials alive past the week they were issued, and making a failed run readable enough to replay rather than guess at. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A Node worker process plus a small web interface over Postgres | the interface exists to read runs; the worker is the product | | Queue | Redis-backed jobs with an explicit idempotency key written per side effect | a queue redelivers, so every step has to survive being run twice | | Steps | Typed step definitions with a validated input and output schema | untyped blobs passed between steps fail at step four with a stack trace and no idea why | | Credentials | OAuth tokens encrypted at rest with the expiry stored beside them | your automation will break on a token long before it breaks on your code | | Model step | One call, structured output, schema-checked, never free text into the next step | a paragraph where the next step expected an identifier is the usual cause of death | | Replay | The trigger payload stored verbatim so a run can be repeated without the trigger | you cannot debug what you cannot re-run | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the worker and the interface. - Get it: nodejs.org or a version manager. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL and Redis** · free locally, a few pounds a month hosted - Why: Postgres holds runs and the side-effect ledger, Redis holds the queue, and the separation is deliberate because one of them must survive the other. - Get it: Docker Compose locally, or a small managed pair. - Verify: A job enqueued in Redis produces a run row in Postgres and survives a Redis restart - [ ] **API credentials for the two services in your workflow** · free - Why: The whole build is shaped by how those two authenticate, and finding out that one of them issues short-lived tokens after you have written the worker is the expensive order to do this in. - Get it: Register an application with each and record the token lifetime from their own documentation. - Verify: A token is issued and its expires_at is stored - [ ] **Your OAuth consent screen publishing status** · free, though verification costs time - Why: A consent screen left in testing can mean a refresh token that expires within days, which turns a working automation into a weekly chore, and the alternative is a verification process with its own cost in time. - Get it: Read the provider's current documentation on refresh token expiry for testing projects and choose deliberately. - Verify: The choice and its consequence are written in the README - [ ] **A recorded trigger payload from each service** · free - Why: Dry runs, replay tests and the whole test suite need a real payload, and capturing one before you build is much easier than reproducing one afterwards. - Get it: Point the webhook at a request bin once and save the raw body. - Verify: The suite runs end to end from the fixture with no network access ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - A library of maintained connectors. - Bulk email from your own domain. - A visual workflow builder. - Multi-tenant credentials and approval chains. - schema drift handling and enterprise controls - sender reputation, deliverability, and suppression handling - hundreds of maintained connectors - OAuth app verification - durable execution at scale If one of those is essential to you, that is the reason to keep paying for Lindy, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Lindy The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me one automation you actually run, with a stored trigger, typed steps, retries that cannot double-charge you and a replay button to replace Lindy. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: A Node worker process plus a small web interface over Postgres · the interface exists to read runs; the worker is the product - Queue: Redis-backed jobs with an explicit idempotency key written per side effect · a queue redelivers, so every step has to survive being run twice - Steps: Typed step definitions with a validated input and output schema · untyped blobs passed between steps fail at step four with a stack trace and no idea why - Credentials: OAuth tokens encrypted at rest with the expiry stored beside them · your automation will break on a token long before it breaks on your code - Model step: One call, structured output, schema-checked, never free text into the next step · a paragraph where the next step expected an identifier is the usual cause of death - Replay: The trigger payload stored verbatim so a run can be repeated without the trigger · you cannot debug what you cannot re-run ### Data model (create this before Phase 1) - workflows(id, name, version, trigger_kind, trigger_config_json, enabled, updated_at) - runs(id, workflow_id, workflow_version, trigger_payload_json, started_at, ended_at, state, replay_of_run_id, dry_run) - steps(id, run_id, ordinal, step_type, input_json, output_json, state, attempts, started_at, ended_at, error_json) - side_effects(id, run_id, step_id, idempotency_key, external_ref, at) · unique on idempotency_key, and the only record that something already happened out in the world - credentials(id, provider, account, access_token_enc, refresh_token_enc, expires_at, scopes_json, last_refreshed_at, status) - dead_letters(id, run_id, step_id, at, reason, payload_json) · where a run goes when retrying would be guessing - workflow_version is copied into every run. Editing a workflow must never change what an old run says it did, which is why the definition is versioned rather than updated in place. ### Phase 1 · One trigger, one run, kept verbatim Build: Support one webhook trigger and one schedule trigger. On receipt, write the run with the raw payload and the current workflow version, then enqueue it. Verify webhook signatures where the sender provides them, and reject a payload whose timestamp is outside a stated tolerance. Trap: Parsing the webhook body and storing the parsed object loses the raw bytes you need to verify the signature, and signature verification against a re-serialised body fails for reasons that take an evening to find; keep the raw body, verify against it, and store it as received so a replay months later feeds the step exactly what the step saw the first time. Done when: The raw request body is stored byte-for-byte and the signature is verified against those bytes, tested with a deliberately re-serialised body that must fail. A payload older than the configured tolerance is rejected with a reason recorded. Every run carries the workflow version that was current when it started, and editing the workflow afterwards does not change it, asserted in a test. A trigger that arrives while the workflow is disabled is recorded and not executed. Do not build yet: No steps, retries, credentials or model calls yet. ### Phase 2 · Steps that survive being run twice Build: Give each step an idempotency key derived from the run id, the step ordinal and a stable hash of its input. Before performing a side effect, look for that key in side_effects. Write the key and the external reference in the same transaction that records the step output. Trap: A worker that loses its lock while busy has its job marked stalled, and the documented behaviour is that the job is moved back to waiting and processed again by another worker, so a step that sends an email or creates an invoice does it twice while your code never saw an error; check for the idempotency key first, write it with the result in one transaction, and confirm the default stall settings in the queue library you chose rather than assuming they give you room. Done when: Running the same step three times produces one row in side_effects and one external call, tested with a stub that counts calls. Killing the worker between the external call and the transaction leaves the run resumable and does not repeat the call, tested by killing at that point. The unique index on idempotency_key exists and a duplicate insert raises rather than being swallowed. A step whose input changes produces a different key, tested. Do not build yet: No model step, credentials or replay yet. ### Phase 3 · The model step, typed on both sides Build: Define the model step with an input schema and an output schema. Call once with structured output, validate, and on a validation failure retry once with the error attached, then fail the run. Store the raw response, the model identifier and the prompt version on the step row. Trap: Letting a model step return a paragraph and passing it to the next step means the automation fails somewhere downstream with a message about an undefined property, and because the model usually returns something reasonable it will pass your testing and break in week three; validate against the schema at the step boundary, treat a validation miss as its own failure kind, and never let unvalidated text become a step output. Done when: A model response that does not validate never becomes a step output, tested with a stub returning prose. The model identifier and prompt version are stored on every step that used a model, so a changed answer can be attributed. A validation failure retries exactly once and then fails the run with the raw response kept. A model step is skippable in dry run and the rest of the workflow still executes against a recorded fixture. Do not build yet: No credential lifecycle, replay or kill switch yet. ### Phase 4 · Credentials, and the seven day cliff Build: Store expires_at and scopes with every token, refresh ahead of expiry, and record every refresh. Alarm when a token is inside the warning window or when a refresh fails. Put the consent screen's publishing status in the README as a decision, not a footnote. Trap: A personal automation against a large provider usually runs on an application whose consent screen is still in testing, and that provider's own documentation says a project with an external user type and a publishing status of testing is issued a refresh token that expires in seven days unless the scopes are a subset of name, email and profile, so your automation dies every single week for reasons that look like your code; store the expiry, alarm on it, and decide deliberately whether to go through verification or to accept a weekly reauthorisation and automate the reminder. Done when: Every credential row has expires_at populated at issue time, asserted by a test that rejects a null. A credential inside the warning window raises a visible alarm and a log entry, tested by moving the clock. A refresh failure marks the credential and pauses the workflows that depend on it rather than failing their runs one by one. The README states the consent screen publishing status, what it implies for token lifetime, and which route you chose. Do not build yet: No replay, dry run or cost ceilings yet. ### Phase 5 · Reading a run, replaying it, stopping it Build: A run timeline showing each step with its input, output, attempts and error. A replay that defaults to dry run and shows what each step would do. A real replay reuses the original idempotency keys. A global kill switch, a per-workflow concurrency of one, and a spend ceiling. Trap: A replay button that simply re-runs the workflow performs every side effect again, so debugging a failed run by replaying it sends the customer a second email and creates a second record; make dry run the default, reuse the original idempotency keys on a real replay so anything already done is skipped, and show a diff of what would change before anything runs. Done when: Replay defaults to dry run and a dry run makes no external call, asserted with a stub that fails the test if called. A real replay reuses the original idempotency keys and skips completed side effects, tested against a half-finished run. The kill switch stops the worker claiming new jobs within a stated number of seconds and lets in-flight steps finish. Reaching the spend or call ceiling pauses the workflow with a message and does not lose queued triggers. Do not build yet: Nothing after this. ### Out of scope (and why) - A connector library. This build supports the two services you actually use, because maintaining hundreds of integrations against changing APIs is the subscription rather than the code. - Sending bulk email from your own domain, which is a deliverability and reputation problem that will cost you more attention than the automation saves. - A visual workflow builder, which is a large interface project that adds nothing to a workflow you can define in one typed file. - Multi-tenant credentials and an approval chain, which turn a personal tool into something that needs an audit story and a security review. ### README must contain - That the queue delivers at least once, and that the idempotency key in side_effects is what makes that safe - That runs pin the workflow version, so editing a workflow never rewrites what an old run reports - That connector tokens expire, what your consent screen's publishing status implies for that, and which route you chose - That replay defaults to dry run and how a real replay reuses the original keys - Where credentials are encrypted, how to rotate the encryption key, and how to revoke one connector ===== AGENTS.md ===== # Agent instructions · Lindy indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A Node worker process plus a small web interface over Postgres, Redis-backed jobs with an explicit idempotency key written per side effect, Typed step definitions with a validated input and output schema, OAuth tokens encrypted at rest with the expiry stored beside them, One call, structured output, schema-checked, never free text into the next step, The trigger payload stored verbatim so a run can be repeated without the trigger. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Parsing the webhook body and storing the parsed object loses the raw bytes you need to verify the signature, and signature verification against a re-serialised body fails for reasons that take an evening to find; keep the raw body, verify against it, and store it as received so a replay months later feeds the step exactly what the step saw the first time. - A worker that loses its lock while busy has its job marked stalled, and the documented behaviour is that the job is moved back to waiting and processed again by another worker, so a step that sends an email or creates an invoice does it twice while your code never saw an error; check for the idempotency key first, write it with the result in one transaction, and confirm the default stall settings in the queue library you chose rather than assuming they give you room. - Letting a model step return a paragraph and passing it to the next step means the automation fails somewhere downstream with a message about an undefined property, and because the model usually returns something reasonable it will pass your testing and break in week three; validate against the schema at the step boundary, treat a validation miss as its own failure kind, and never let unvalidated text become a step output. - A personal automation against a large provider usually runs on an application whose consent screen is still in testing, and that provider's own documentation says a project with an external user type and a publishing status of testing is issued a refresh token that expires in seven days unless the scopes are a subset of name, email and profile, so your automation dies every single week for reasons that look like your code; store the expiry, alarm on it, and decide deliberately whether to go through verification or to accept a weekly reauthorisation and automate the reminder. ===== BUILD_PLAN.md ===== # Build plan · Lindy A single-workflow automation runner whose hard problems are surviving at-least-once delivery without repeating a side effect, keeping connector credentials alive past the week they were issued, and making a failed run readable enough to replay rather than guess at. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · One trigger, one run, kept verbatim A trigger creates a run row holding the exact payload that caused it, before any step executes. ### Steps 1. Store the raw body before parsing anything Signature verification and replay both need the original bytes. Files: `src/triggers/webhook.ts`, `tests/signature.test.ts` 2. Copy the workflow version into the run An old run must keep describing what it actually did. Files: `src/db/schema.sql`, `src/runs/create.ts` 3. Reject stale payloads with a recorded reason A replayed webhook from last month is an attack, not a trigger. Files: `src/triggers/webhook.ts` ### Done when - [ ] The raw body is stored and verified byte-for-byte - [ ] Stale payloads are rejected with a reason - [ ] Run rows pin the workflow version - [ ] Triggers on a disabled workflow are recorded, not run ### Watch out - Parsing the webhook body and storing the parsed object loses the raw bytes you need to verify the signature, and signature verification against a re-serialised body fails for reasons that take an evening to find; keep the raw body, verify against it, and store it as received so a replay months later feeds the step exactly what the step saw the first time. ## Phase 2 · Steps that survive being run twice Every side effect happens exactly once even when the queue delivers the job three times. ### Steps 1. Derive the key from run, ordinal and input hash Stable across retries, different across inputs. Files: `src/steps/idempotency.ts`, `tests/idempotency.test.ts` 2. Check the key, then act, then write in one transaction The order is the whole mechanism. Files: `src/steps/run-step.ts` 3. Read your queue's stall and retry defaults and write them down You are going to be redelivered whether you planned for it or not. Files: `docs/queue-behaviour.md` ### Done when - [ ] Three executions produce one external call - [ ] A kill mid-step never repeats the call - [ ] Duplicate idempotency keys raise - [ ] A changed input yields a different key ### Watch out - A worker that loses its lock while busy has its job marked stalled, and the documented behaviour is that the job is moved back to waiting and processed again by another worker, so a step that sends an email or creates an invoice does it twice while your code never saw an error; check for the idempotency key first, write it with the result in one transaction, and confirm the default stall settings in the queue library you chose rather than assuming they give you room. ## Phase 3 · The model step, typed on both sides A model call either produces schema-valid structured data or stops the run. ### Steps 1. Schema-check the output at the step boundary The boundary is the only place this is cheap to catch. Files: `src/steps/model.ts`, `tests/model-schema.test.ts` 2. Store model id and prompt version on the step So a different answer next month has an explanation. Files: `src/db/schema.sql` 3. Retry once with the validation error attached, then stop A third attempt at the same bad shape is just money. Files: `src/steps/model.ts` ### Done when - [ ] Unvalidated text never becomes a step output - [ ] Model id and prompt version are recorded - [ ] Validation failure retries once and then fails - [ ] Dry run substitutes a recorded fixture ### Watch out - Letting a model step return a paragraph and passing it to the next step means the automation fails somewhere downstream with a message about an undefined property, and because the model usually returns something reasonable it will pass your testing and break in week three; validate against the schema at the step boundary, treat a validation miss as its own failure kind, and never let unvalidated text become a step output. ## Phase 4 · Credentials, and the seven day cliff A connector token's expiry is known in advance and never discovered at run time. ### Steps 1. Record expires_at and scopes at issue time A token with an unknown expiry is a token that will surprise you. Files: `src/credentials/store.ts`, `tests/expiry.test.ts` 2. Alarm before the cliff, pause the workflow at it One alarm beats forty identical run failures. Files: `src/credentials/watch.ts` 3. Write down the publishing status decision Verification or weekly reauthorisation. Both are fine; drifting is not. Files: `docs/oauth-decision.md` ### Done when - [ ] No credential row has a null expiry - [ ] The warning window raises an alarm - [ ] A refresh failure pauses rather than fails runs - [ ] The publishing status decision is documented ### Watch out - A personal automation against a large provider usually runs on an application whose consent screen is still in testing, and that provider's own documentation says a project with an external user type and a publishing status of testing is issued a refresh token that expires in seven days unless the scopes are a subset of name, email and profile, so your automation dies every single week for reasons that look like your code; store the expiry, alarm on it, and decide deliberately whether to go through verification or to accept a weekly reauthorisation and automate the reminder. ## Not in this build - A library of maintained connectors. - Bulk email from your own domain. - A visual workflow builder. - Multi-tenant credentials and approval chains. ## After v1, if you want it - A second workflow, which is the only real test of whether your step types were general or accidental - A weekly digest of runs, failures and spend, built from the tables you already have - Recorded fixtures captured automatically from production runs, so the test suite grows from real payloads ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Runs, steps and the side-effect ledger. This is the one that must not be lost. DATABASE_URL=postgres://localhost:5432/automation # Required · secret. The queue. Losing it loses queued work, not history. REDIS_URL=redis://localhost:6379 # Required · secret. Encrypts connector tokens at rest. Rotating it means re-encrypting every row. CREDENTIAL_ENC_KEY=base64:...redacted # Required · secret. Verified against the raw body, not the parsed object. WEBHOOK_SIGNING_SECRET=whsec_...redacted # Required. Payloads outside this window are rejected as stale. WEBHOOK_TOLERANCE_SECONDS=300 # Required. How early a dying connector token raises an alarm. TOKEN_WARN_DAYS=2 # Required. Model spend for the month. At the ceiling the workflow pauses. MONTHLY_COST_CEILING=10.00 # Required. Set to on and the worker stops claiming jobs within seconds. KILL_SWITCH=off
You are building a lean indie version of Lindy. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Lindy · indie build A single-workflow automation runner whose hard problems are surviving at-least-once delivery without repeating a side effect, keeping connector credentials alive past the week they were issued, and making a failed run readable enough to replay rather than guess at. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A Node worker process plus a small web interface over Postgres | the interface exists to read runs; the worker is the product | | Queue | Redis-backed jobs with an explicit idempotency key written per side effect | a queue redelivers, so every step has to survive being run twice | | Steps | Typed step definitions with a validated input and output schema | untyped blobs passed between steps fail at step four with a stack trace and no idea why | | Credentials | OAuth tokens encrypted at rest with the expiry stored beside them | your automation will break on a token long before it breaks on your code | | Model step | One call, structured output, schema-checked, never free text into the next step | a paragraph where the next step expected an identifier is the usual cause of death | | Replay | The trigger payload stored verbatim so a run can be repeated without the trigger | you cannot debug what you cannot re-run | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the worker and the interface. - Get it: nodejs.org or a version manager. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL and Redis** · free locally, a few pounds a month hosted - Why: Postgres holds runs and the side-effect ledger, Redis holds the queue, and the separation is deliberate because one of them must survive the other. - Get it: Docker Compose locally, or a small managed pair. - Verify: A job enqueued in Redis produces a run row in Postgres and survives a Redis restart - [ ] **API credentials for the two services in your workflow** · free - Why: The whole build is shaped by how those two authenticate, and finding out that one of them issues short-lived tokens after you have written the worker is the expensive order to do this in. - Get it: Register an application with each and record the token lifetime from their own documentation. - Verify: A token is issued and its expires_at is stored - [ ] **Your OAuth consent screen publishing status** · free, though verification costs time - Why: A consent screen left in testing can mean a refresh token that expires within days, which turns a working automation into a weekly chore, and the alternative is a verification process with its own cost in time. - Get it: Read the provider's current documentation on refresh token expiry for testing projects and choose deliberately. - Verify: The choice and its consequence are written in the README - [ ] **A recorded trigger payload from each service** · free - Why: Dry runs, replay tests and the whole test suite need a real payload, and capturing one before you build is much easier than reproducing one afterwards. - Get it: Point the webhook at a request bin once and save the raw body. - Verify: The suite runs end to end from the fixture with no network access ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - A library of maintained connectors. - Bulk email from your own domain. - A visual workflow builder. - Multi-tenant credentials and approval chains. - schema drift handling and enterprise controls - sender reputation, deliverability, and suppression handling - hundreds of maintained connectors - OAuth app verification - durable execution at scale If one of those is essential to you, that is the reason to keep paying for Lindy, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Lindy The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me one automation you actually run, with a stored trigger, typed steps, retries that cannot double-charge you and a replay button to replace Lindy. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: A Node worker process plus a small web interface over Postgres · the interface exists to read runs; the worker is the product - Queue: Redis-backed jobs with an explicit idempotency key written per side effect · a queue redelivers, so every step has to survive being run twice - Steps: Typed step definitions with a validated input and output schema · untyped blobs passed between steps fail at step four with a stack trace and no idea why - Credentials: OAuth tokens encrypted at rest with the expiry stored beside them · your automation will break on a token long before it breaks on your code - Model step: One call, structured output, schema-checked, never free text into the next step · a paragraph where the next step expected an identifier is the usual cause of death - Replay: The trigger payload stored verbatim so a run can be repeated without the trigger · you cannot debug what you cannot re-run ### Data model (create this before Phase 1) - workflows(id, name, version, trigger_kind, trigger_config_json, enabled, updated_at) - runs(id, workflow_id, workflow_version, trigger_payload_json, started_at, ended_at, state, replay_of_run_id, dry_run) - steps(id, run_id, ordinal, step_type, input_json, output_json, state, attempts, started_at, ended_at, error_json) - side_effects(id, run_id, step_id, idempotency_key, external_ref, at) · unique on idempotency_key, and the only record that something already happened out in the world - credentials(id, provider, account, access_token_enc, refresh_token_enc, expires_at, scopes_json, last_refreshed_at, status) - dead_letters(id, run_id, step_id, at, reason, payload_json) · where a run goes when retrying would be guessing - workflow_version is copied into every run. Editing a workflow must never change what an old run says it did, which is why the definition is versioned rather than updated in place. ### Phase 1 · One trigger, one run, kept verbatim Build: Support one webhook trigger and one schedule trigger. On receipt, write the run with the raw payload and the current workflow version, then enqueue it. Verify webhook signatures where the sender provides them, and reject a payload whose timestamp is outside a stated tolerance. Trap: Parsing the webhook body and storing the parsed object loses the raw bytes you need to verify the signature, and signature verification against a re-serialised body fails for reasons that take an evening to find; keep the raw body, verify against it, and store it as received so a replay months later feeds the step exactly what the step saw the first time. Done when: The raw request body is stored byte-for-byte and the signature is verified against those bytes, tested with a deliberately re-serialised body that must fail. A payload older than the configured tolerance is rejected with a reason recorded. Every run carries the workflow version that was current when it started, and editing the workflow afterwards does not change it, asserted in a test. A trigger that arrives while the workflow is disabled is recorded and not executed. Do not build yet: No steps, retries, credentials or model calls yet. ### Phase 2 · Steps that survive being run twice Build: Give each step an idempotency key derived from the run id, the step ordinal and a stable hash of its input. Before performing a side effect, look for that key in side_effects. Write the key and the external reference in the same transaction that records the step output. Trap: A worker that loses its lock while busy has its job marked stalled, and the documented behaviour is that the job is moved back to waiting and processed again by another worker, so a step that sends an email or creates an invoice does it twice while your code never saw an error; check for the idempotency key first, write it with the result in one transaction, and confirm the default stall settings in the queue library you chose rather than assuming they give you room. Done when: Running the same step three times produces one row in side_effects and one external call, tested with a stub that counts calls. Killing the worker between the external call and the transaction leaves the run resumable and does not repeat the call, tested by killing at that point. The unique index on idempotency_key exists and a duplicate insert raises rather than being swallowed. A step whose input changes produces a different key, tested. Do not build yet: No model step, credentials or replay yet. ### Phase 3 · The model step, typed on both sides Build: Define the model step with an input schema and an output schema. Call once with structured output, validate, and on a validation failure retry once with the error attached, then fail the run. Store the raw response, the model identifier and the prompt version on the step row. Trap: Letting a model step return a paragraph and passing it to the next step means the automation fails somewhere downstream with a message about an undefined property, and because the model usually returns something reasonable it will pass your testing and break in week three; validate against the schema at the step boundary, treat a validation miss as its own failure kind, and never let unvalidated text become a step output. Done when: A model response that does not validate never becomes a step output, tested with a stub returning prose. The model identifier and prompt version are stored on every step that used a model, so a changed answer can be attributed. A validation failure retries exactly once and then fails the run with the raw response kept. A model step is skippable in dry run and the rest of the workflow still executes against a recorded fixture. Do not build yet: No credential lifecycle, replay or kill switch yet. ### Phase 4 · Credentials, and the seven day cliff Build: Store expires_at and scopes with every token, refresh ahead of expiry, and record every refresh. Alarm when a token is inside the warning window or when a refresh fails. Put the consent screen's publishing status in the README as a decision, not a footnote. Trap: A personal automation against a large provider usually runs on an application whose consent screen is still in testing, and that provider's own documentation says a project with an external user type and a publishing status of testing is issued a refresh token that expires in seven days unless the scopes are a subset of name, email and profile, so your automation dies every single week for reasons that look like your code; store the expiry, alarm on it, and decide deliberately whether to go through verification or to accept a weekly reauthorisation and automate the reminder. Done when: Every credential row has expires_at populated at issue time, asserted by a test that rejects a null. A credential inside the warning window raises a visible alarm and a log entry, tested by moving the clock. A refresh failure marks the credential and pauses the workflows that depend on it rather than failing their runs one by one. The README states the consent screen publishing status, what it implies for token lifetime, and which route you chose. Do not build yet: No replay, dry run or cost ceilings yet. ### Phase 5 · Reading a run, replaying it, stopping it Build: A run timeline showing each step with its input, output, attempts and error. A replay that defaults to dry run and shows what each step would do. A real replay reuses the original idempotency keys. A global kill switch, a per-workflow concurrency of one, and a spend ceiling. Trap: A replay button that simply re-runs the workflow performs every side effect again, so debugging a failed run by replaying it sends the customer a second email and creates a second record; make dry run the default, reuse the original idempotency keys on a real replay so anything already done is skipped, and show a diff of what would change before anything runs. Done when: Replay defaults to dry run and a dry run makes no external call, asserted with a stub that fails the test if called. A real replay reuses the original idempotency keys and skips completed side effects, tested against a half-finished run. The kill switch stops the worker claiming new jobs within a stated number of seconds and lets in-flight steps finish. Reaching the spend or call ceiling pauses the workflow with a message and does not lose queued triggers. Do not build yet: Nothing after this. ### Out of scope (and why) - A connector library. This build supports the two services you actually use, because maintaining hundreds of integrations against changing APIs is the subscription rather than the code. - Sending bulk email from your own domain, which is a deliverability and reputation problem that will cost you more attention than the automation saves. - A visual workflow builder, which is a large interface project that adds nothing to a workflow you can define in one typed file. - Multi-tenant credentials and an approval chain, which turn a personal tool into something that needs an audit story and a security review. ### README must contain - That the queue delivers at least once, and that the idempotency key in side_effects is what makes that safe - That runs pin the workflow version, so editing a workflow never rewrites what an old run reports - That connector tokens expire, what your consent screen's publishing status implies for that, and which route you chose - That replay defaults to dry run and how a real replay reuses the original keys - Where credentials are encrypted, how to rotate the encryption key, and how to revoke one connector ===== AGENTS.md ===== # Agent instructions · Lindy indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A Node worker process plus a small web interface over Postgres, Redis-backed jobs with an explicit idempotency key written per side effect, Typed step definitions with a validated input and output schema, OAuth tokens encrypted at rest with the expiry stored beside them, One call, structured output, schema-checked, never free text into the next step, The trigger payload stored verbatim so a run can be repeated without the trigger. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Parsing the webhook body and storing the parsed object loses the raw bytes you need to verify the signature, and signature verification against a re-serialised body fails for reasons that take an evening to find; keep the raw body, verify against it, and store it as received so a replay months later feeds the step exactly what the step saw the first time. - A worker that loses its lock while busy has its job marked stalled, and the documented behaviour is that the job is moved back to waiting and processed again by another worker, so a step that sends an email or creates an invoice does it twice while your code never saw an error; check for the idempotency key first, write it with the result in one transaction, and confirm the default stall settings in the queue library you chose rather than assuming they give you room. - Letting a model step return a paragraph and passing it to the next step means the automation fails somewhere downstream with a message about an undefined property, and because the model usually returns something reasonable it will pass your testing and break in week three; validate against the schema at the step boundary, treat a validation miss as its own failure kind, and never let unvalidated text become a step output. - A personal automation against a large provider usually runs on an application whose consent screen is still in testing, and that provider's own documentation says a project with an external user type and a publishing status of testing is issued a refresh token that expires in seven days unless the scopes are a subset of name, email and profile, so your automation dies every single week for reasons that look like your code; store the expiry, alarm on it, and decide deliberately whether to go through verification or to accept a weekly reauthorisation and automate the reminder. ===== BUILD_PLAN.md ===== # Build plan · Lindy A single-workflow automation runner whose hard problems are surviving at-least-once delivery without repeating a side effect, keeping connector credentials alive past the week they were issued, and making a failed run readable enough to replay rather than guess at. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · One trigger, one run, kept verbatim A trigger creates a run row holding the exact payload that caused it, before any step executes. ### Steps 1. Store the raw body before parsing anything Signature verification and replay both need the original bytes. Files: `src/triggers/webhook.ts`, `tests/signature.test.ts` 2. Copy the workflow version into the run An old run must keep describing what it actually did. Files: `src/db/schema.sql`, `src/runs/create.ts` 3. Reject stale payloads with a recorded reason A replayed webhook from last month is an attack, not a trigger. Files: `src/triggers/webhook.ts` ### Done when - [ ] The raw body is stored and verified byte-for-byte - [ ] Stale payloads are rejected with a reason - [ ] Run rows pin the workflow version - [ ] Triggers on a disabled workflow are recorded, not run ### Watch out - Parsing the webhook body and storing the parsed object loses the raw bytes you need to verify the signature, and signature verification against a re-serialised body fails for reasons that take an evening to find; keep the raw body, verify against it, and store it as received so a replay months later feeds the step exactly what the step saw the first time. ## Phase 2 · Steps that survive being run twice Every side effect happens exactly once even when the queue delivers the job three times. ### Steps 1. Derive the key from run, ordinal and input hash Stable across retries, different across inputs. Files: `src/steps/idempotency.ts`, `tests/idempotency.test.ts` 2. Check the key, then act, then write in one transaction The order is the whole mechanism. Files: `src/steps/run-step.ts` 3. Read your queue's stall and retry defaults and write them down You are going to be redelivered whether you planned for it or not. Files: `docs/queue-behaviour.md` ### Done when - [ ] Three executions produce one external call - [ ] A kill mid-step never repeats the call - [ ] Duplicate idempotency keys raise - [ ] A changed input yields a different key ### Watch out - A worker that loses its lock while busy has its job marked stalled, and the documented behaviour is that the job is moved back to waiting and processed again by another worker, so a step that sends an email or creates an invoice does it twice while your code never saw an error; check for the idempotency key first, write it with the result in one transaction, and confirm the default stall settings in the queue library you chose rather than assuming they give you room. ## Phase 3 · The model step, typed on both sides A model call either produces schema-valid structured data or stops the run. ### Steps 1. Schema-check the output at the step boundary The boundary is the only place this is cheap to catch. Files: `src/steps/model.ts`, `tests/model-schema.test.ts` 2. Store model id and prompt version on the step So a different answer next month has an explanation. Files: `src/db/schema.sql` 3. Retry once with the validation error attached, then stop A third attempt at the same bad shape is just money. Files: `src/steps/model.ts` ### Done when - [ ] Unvalidated text never becomes a step output - [ ] Model id and prompt version are recorded - [ ] Validation failure retries once and then fails - [ ] Dry run substitutes a recorded fixture ### Watch out - Letting a model step return a paragraph and passing it to the next step means the automation fails somewhere downstream with a message about an undefined property, and because the model usually returns something reasonable it will pass your testing and break in week three; validate against the schema at the step boundary, treat a validation miss as its own failure kind, and never let unvalidated text become a step output. ## Phase 4 · Credentials, and the seven day cliff A connector token's expiry is known in advance and never discovered at run time. ### Steps 1. Record expires_at and scopes at issue time A token with an unknown expiry is a token that will surprise you. Files: `src/credentials/store.ts`, `tests/expiry.test.ts` 2. Alarm before the cliff, pause the workflow at it One alarm beats forty identical run failures. Files: `src/credentials/watch.ts` 3. Write down the publishing status decision Verification or weekly reauthorisation. Both are fine; drifting is not. Files: `docs/oauth-decision.md` ### Done when - [ ] No credential row has a null expiry - [ ] The warning window raises an alarm - [ ] A refresh failure pauses rather than fails runs - [ ] The publishing status decision is documented ### Watch out - A personal automation against a large provider usually runs on an application whose consent screen is still in testing, and that provider's own documentation says a project with an external user type and a publishing status of testing is issued a refresh token that expires in seven days unless the scopes are a subset of name, email and profile, so your automation dies every single week for reasons that look like your code; store the expiry, alarm on it, and decide deliberately whether to go through verification or to accept a weekly reauthorisation and automate the reminder. ## Not in this build - A library of maintained connectors. - Bulk email from your own domain. - A visual workflow builder. - Multi-tenant credentials and approval chains. ## After v1, if you want it - A second workflow, which is the only real test of whether your step types were general or accidental - A weekly digest of runs, failures and spend, built from the tables you already have - Recorded fixtures captured automatically from production runs, so the test suite grows from real payloads ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Runs, steps and the side-effect ledger. This is the one that must not be lost. DATABASE_URL=postgres://localhost:5432/automation # Required · secret. The queue. Losing it loses queued work, not history. REDIS_URL=redis://localhost:6379 # Required · secret. Encrypts connector tokens at rest. Rotating it means re-encrypting every row. CREDENTIAL_ENC_KEY=base64:...redacted # Required · secret. Verified against the raw body, not the parsed object. WEBHOOK_SIGNING_SECRET=whsec_...redacted # Required. Payloads outside this window are rejected as stale. WEBHOOK_TOLERANCE_SECONDS=300 # Required. How early a dying connector token raises an alarm. TOKEN_WARN_DAYS=2 # Required. Model spend for the month. At the ceiling the workflow pauses. MONTHLY_COST_CEILING=10.00 # Required. Set to on and the worker stops claiming jobs within seconds. KILL_SWITCH=off
You are building a production product version of Lindy. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Lindy · product brief ## Problem The visible AI assistants + automation loop is buildable, but a credible replacement needs more than the first screen. Lindy earns its keep through connectors, auth, reliability, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome One automation that records exactly what triggered it, performs each side effect once whatever the queue does, tells you a week before its credentials expire, and can be replayed as a dry run before it is replayed for real. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - hosted PostgreSQL and Redis - API credentials for the two chosen services - Node.js 22 ## Explicit non-goals for v1 - A library of maintained connectors. - Bulk email from your own domain. - A visual workflow builder. - Multi-tenant credentials and approval chains. - schema drift handling and enterprise controls - sender reputation, deliverability, and suppression handling - hundreds of maintained connectors - OAuth app verification - durable execution at scale ## Success criteria - Three executions of a step are confirmed to produce one external call - A worker kill mid-step is confirmed not to repeat the call - Unvalidated model text is confirmed never to become a step output - No credential row is confirmed to have a null expiry - Replay is confirmed to default to dry run and make no calls ===== BRIEF.md ===== # Build brief · Lindy The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me one automation you actually run, with a stored trigger, typed steps, retries that cannot double-charge you and a replay button to replace Lindy. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: A Node worker process plus a small web interface over Postgres · the interface exists to read runs; the worker is the product - Queue: Redis-backed jobs with an explicit idempotency key written per side effect · a queue redelivers, so every step has to survive being run twice - Steps: Typed step definitions with a validated input and output schema · untyped blobs passed between steps fail at step four with a stack trace and no idea why - Credentials: OAuth tokens encrypted at rest with the expiry stored beside them · your automation will break on a token long before it breaks on your code - Model step: One call, structured output, schema-checked, never free text into the next step · a paragraph where the next step expected an identifier is the usual cause of death - Replay: The trigger payload stored verbatim so a run can be repeated without the trigger · you cannot debug what you cannot re-run ### Data model (create this before Phase 1) - workflows(id, name, version, trigger_kind, trigger_config_json, enabled, updated_at) - runs(id, workflow_id, workflow_version, trigger_payload_json, started_at, ended_at, state, replay_of_run_id, dry_run) - steps(id, run_id, ordinal, step_type, input_json, output_json, state, attempts, started_at, ended_at, error_json) - side_effects(id, run_id, step_id, idempotency_key, external_ref, at) · unique on idempotency_key, and the only record that something already happened out in the world - credentials(id, provider, account, access_token_enc, refresh_token_enc, expires_at, scopes_json, last_refreshed_at, status) - dead_letters(id, run_id, step_id, at, reason, payload_json) · where a run goes when retrying would be guessing - workflow_version is copied into every run. Editing a workflow must never change what an old run says it did, which is why the definition is versioned rather than updated in place. ### Phase 1 · One trigger, one run, kept verbatim Build: Support one webhook trigger and one schedule trigger. On receipt, write the run with the raw payload and the current workflow version, then enqueue it. Verify webhook signatures where the sender provides them, and reject a payload whose timestamp is outside a stated tolerance. Trap: Parsing the webhook body and storing the parsed object loses the raw bytes you need to verify the signature, and signature verification against a re-serialised body fails for reasons that take an evening to find; keep the raw body, verify against it, and store it as received so a replay months later feeds the step exactly what the step saw the first time. Done when: The raw request body is stored byte-for-byte and the signature is verified against those bytes, tested with a deliberately re-serialised body that must fail. A payload older than the configured tolerance is rejected with a reason recorded. Every run carries the workflow version that was current when it started, and editing the workflow afterwards does not change it, asserted in a test. A trigger that arrives while the workflow is disabled is recorded and not executed. Do not build yet: No steps, retries, credentials or model calls yet. ### Phase 2 · Steps that survive being run twice Build: Give each step an idempotency key derived from the run id, the step ordinal and a stable hash of its input. Before performing a side effect, look for that key in side_effects. Write the key and the external reference in the same transaction that records the step output. Trap: A worker that loses its lock while busy has its job marked stalled, and the documented behaviour is that the job is moved back to waiting and processed again by another worker, so a step that sends an email or creates an invoice does it twice while your code never saw an error; check for the idempotency key first, write it with the result in one transaction, and confirm the default stall settings in the queue library you chose rather than assuming they give you room. Done when: Running the same step three times produces one row in side_effects and one external call, tested with a stub that counts calls. Killing the worker between the external call and the transaction leaves the run resumable and does not repeat the call, tested by killing at that point. The unique index on idempotency_key exists and a duplicate insert raises rather than being swallowed. A step whose input changes produces a different key, tested. Do not build yet: No model step, credentials or replay yet. ### Phase 3 · The model step, typed on both sides Build: Define the model step with an input schema and an output schema. Call once with structured output, validate, and on a validation failure retry once with the error attached, then fail the run. Store the raw response, the model identifier and the prompt version on the step row. Trap: Letting a model step return a paragraph and passing it to the next step means the automation fails somewhere downstream with a message about an undefined property, and because the model usually returns something reasonable it will pass your testing and break in week three; validate against the schema at the step boundary, treat a validation miss as its own failure kind, and never let unvalidated text become a step output. Done when: A model response that does not validate never becomes a step output, tested with a stub returning prose. The model identifier and prompt version are stored on every step that used a model, so a changed answer can be attributed. A validation failure retries exactly once and then fails the run with the raw response kept. A model step is skippable in dry run and the rest of the workflow still executes against a recorded fixture. Do not build yet: No credential lifecycle, replay or kill switch yet. ### Phase 4 · Credentials, and the seven day cliff Build: Store expires_at and scopes with every token, refresh ahead of expiry, and record every refresh. Alarm when a token is inside the warning window or when a refresh fails. Put the consent screen's publishing status in the README as a decision, not a footnote. Trap: A personal automation against a large provider usually runs on an application whose consent screen is still in testing, and that provider's own documentation says a project with an external user type and a publishing status of testing is issued a refresh token that expires in seven days unless the scopes are a subset of name, email and profile, so your automation dies every single week for reasons that look like your code; store the expiry, alarm on it, and decide deliberately whether to go through verification or to accept a weekly reauthorisation and automate the reminder. Done when: Every credential row has expires_at populated at issue time, asserted by a test that rejects a null. A credential inside the warning window raises a visible alarm and a log entry, tested by moving the clock. A refresh failure marks the credential and pauses the workflows that depend on it rather than failing their runs one by one. The README states the consent screen publishing status, what it implies for token lifetime, and which route you chose. Do not build yet: No replay, dry run or cost ceilings yet. ### Phase 5 · Reading a run, replaying it, stopping it Build: A run timeline showing each step with its input, output, attempts and error. A replay that defaults to dry run and shows what each step would do. A real replay reuses the original idempotency keys. A global kill switch, a per-workflow concurrency of one, and a spend ceiling. Trap: A replay button that simply re-runs the workflow performs every side effect again, so debugging a failed run by replaying it sends the customer a second email and creates a second record; make dry run the default, reuse the original idempotency keys on a real replay so anything already done is skipped, and show a diff of what would change before anything runs. Done when: Replay defaults to dry run and a dry run makes no external call, asserted with a stub that fails the test if called. A real replay reuses the original idempotency keys and skips completed side effects, tested against a half-finished run. The kill switch stops the worker claiming new jobs within a stated number of seconds and lets in-flight steps finish. Reaching the spend or call ceiling pauses the workflow with a message and does not lose queued triggers. Do not build yet: Nothing after this. ### Out of scope (and why) - A connector library. This build supports the two services you actually use, because maintaining hundreds of integrations against changing APIs is the subscription rather than the code. - Sending bulk email from your own domain, which is a deliverability and reputation problem that will cost you more attention than the automation saves. - A visual workflow builder, which is a large interface project that adds nothing to a workflow you can define in one typed file. - Multi-tenant credentials and an approval chain, which turn a personal tool into something that needs an audit story and a security review. ### README must contain - That the queue delivers at least once, and that the idempotency key in side_effects is what makes that safe - That runs pin the workflow version, so editing a workflow never rewrites what an old run reports - That connector tokens expire, what your consent screen's publishing status implies for that, and which route you chose - That replay defaults to dry run and how a real replay reuses the original keys - Where credentials are encrypted, how to rotate the encryption key, and how to revoke one connector ===== ARCHITECTURE.md ===== # Architecture · Lindy ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A Node worker process plus a small web interface over Postgres | the interface exists to read runs; the worker is the product | | Queue | Redis-backed jobs with an explicit idempotency key written per side effect | a queue redelivers, so every step has to survive being run twice | | Steps | Typed step definitions with a validated input and output schema | untyped blobs passed between steps fail at step four with a stack trace and no idea why | | Credentials | OAuth tokens encrypted at rest with the expiry stored beside them | your automation will break on a token long before it breaks on your code | | Model step | One call, structured output, schema-checked, never free text into the next step | a paragraph where the next step expected an identifier is the usual cause of death | | Replay | The trigger payload stored verbatim so a run can be repeated without the trigger | you cannot debug what you cannot re-run | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Triggers | Webhook and schedule intake, raw payload storage. | Any trigger, provided the raw bytes are stored before parsing. | | Engine | Step execution, attempts, states, dead letters. | Any executor, provided every side effect is keyed. | | Ledger | side_effects and its unique index. | Not swappable. This table is the reason retries are safe. | | Credentials | Encrypted tokens, refresh, expiry alarms. | Any secret store, provided expiry is recorded at issue. | | Console | Run timelines, replay, kill switch, ceilings. | Any interface, provided replay defaults to dry run. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Runs, steps and the side-effect ledger. This is the one that must not be lost. - `REDIS_URL` · required, secret · The queue. Losing it loses queued work, not history. - `CREDENTIAL_ENC_KEY` · required, secret · Encrypts connector tokens at rest. Rotating it means re-encrypting every row. - `WEBHOOK_SIGNING_SECRET` · required, secret · Verified against the raw body, not the parsed object. - `WEBHOOK_TOLERANCE_SECONDS` · required · Payloads outside this window are rejected as stale. - `TOKEN_WARN_DAYS` · required · How early a dying connector token raises an alarm. - `MONTHLY_COST_CEILING` · required · Model spend for the month. At the ceiling the workflow pauses. - `KILL_SWITCH` · required · Set to on and the worker stops claiming jobs within seconds. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Lindy product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: A Node worker process plus a small web interface over Postgres, Redis-backed jobs with an explicit idempotency key written per side effect, Typed step definitions with a validated input and output schema, OAuth tokens encrypted at rest with the expiry stored beside them, One call, structured output, schema-checked, never free text into the next step, The trigger payload stored verbatim so a run can be repeated without the trigger. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Parsing the webhook body and storing the parsed object loses the raw bytes you need to verify the signature, and signature verification against a re-serialised body fails for reasons that take an evening to find; keep the raw body, verify against it, and store it as received so a replay months later feeds the step exactly what the step saw the first time. - A worker that loses its lock while busy has its job marked stalled, and the documented behaviour is that the job is moved back to waiting and processed again by another worker, so a step that sends an email or creates an invoice does it twice while your code never saw an error; check for the idempotency key first, write it with the result in one transaction, and confirm the default stall settings in the queue library you chose rather than assuming they give you room. - Letting a model step return a paragraph and passing it to the next step means the automation fails somewhere downstream with a message about an undefined property, and because the model usually returns something reasonable it will pass your testing and break in week three; validate against the schema at the step boundary, treat a validation miss as its own failure kind, and never let unvalidated text become a step output. - A personal automation against a large provider usually runs on an application whose consent screen is still in testing, and that provider's own documentation says a project with an external user type and a publishing status of testing is issued a refresh token that expires in seven days unless the scopes are a subset of name, email and profile, so your automation dies every single week for reasons that look like your code; store the expiry, alarm on it, and decide deliberately whether to go through verification or to accept a weekly reauthorisation and automate the reminder. - A replay button that simply re-runs the workflow performs every side effect again, so debugging a failed run by replaying it sends the customer a second email and creates a second record; make dry run the default, reuse the original idempotency keys on a real replay so anything already done is skipped, and show a diff of what would change before anything runs. ===== MILESTONES.md ===== # Delivery milestones · Lindy Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · One trigger, one run, kept verbatim A trigger creates a run row holding the exact payload that caused it, before any step executes. ### Steps 1. Store the raw body before parsing anything Signature verification and replay both need the original bytes. Files: `src/triggers/webhook.ts`, `tests/signature.test.ts` 2. Copy the workflow version into the run An old run must keep describing what it actually did. Files: `src/db/schema.sql`, `src/runs/create.ts` 3. Reject stale payloads with a recorded reason A replayed webhook from last month is an attack, not a trigger. Files: `src/triggers/webhook.ts` ### Done when - [ ] The raw body is stored and verified byte-for-byte - [ ] Stale payloads are rejected with a reason - [ ] Run rows pin the workflow version - [ ] Triggers on a disabled workflow are recorded, not run ### Watch out - Parsing the webhook body and storing the parsed object loses the raw bytes you need to verify the signature, and signature verification against a re-serialised body fails for reasons that take an evening to find; keep the raw body, verify against it, and store it as received so a replay months later feeds the step exactly what the step saw the first time. ## M2 · Steps that survive being run twice Every side effect happens exactly once even when the queue delivers the job three times. ### Steps 1. Derive the key from run, ordinal and input hash Stable across retries, different across inputs. Files: `src/steps/idempotency.ts`, `tests/idempotency.test.ts` 2. Check the key, then act, then write in one transaction The order is the whole mechanism. Files: `src/steps/run-step.ts` 3. Read your queue's stall and retry defaults and write them down You are going to be redelivered whether you planned for it or not. Files: `docs/queue-behaviour.md` ### Done when - [ ] Three executions produce one external call - [ ] A kill mid-step never repeats the call - [ ] Duplicate idempotency keys raise - [ ] A changed input yields a different key ### Watch out - A worker that loses its lock while busy has its job marked stalled, and the documented behaviour is that the job is moved back to waiting and processed again by another worker, so a step that sends an email or creates an invoice does it twice while your code never saw an error; check for the idempotency key first, write it with the result in one transaction, and confirm the default stall settings in the queue library you chose rather than assuming they give you room. ## M3 · The model step, typed on both sides A model call either produces schema-valid structured data or stops the run. ### Steps 1. Schema-check the output at the step boundary The boundary is the only place this is cheap to catch. Files: `src/steps/model.ts`, `tests/model-schema.test.ts` 2. Store model id and prompt version on the step So a different answer next month has an explanation. Files: `src/db/schema.sql` 3. Retry once with the validation error attached, then stop A third attempt at the same bad shape is just money. Files: `src/steps/model.ts` ### Done when - [ ] Unvalidated text never becomes a step output - [ ] Model id and prompt version are recorded - [ ] Validation failure retries once and then fails - [ ] Dry run substitutes a recorded fixture ### Watch out - Letting a model step return a paragraph and passing it to the next step means the automation fails somewhere downstream with a message about an undefined property, and because the model usually returns something reasonable it will pass your testing and break in week three; validate against the schema at the step boundary, treat a validation miss as its own failure kind, and never let unvalidated text become a step output. ## M4 · Credentials, and the seven day cliff A connector token's expiry is known in advance and never discovered at run time. ### Steps 1. Record expires_at and scopes at issue time A token with an unknown expiry is a token that will surprise you. Files: `src/credentials/store.ts`, `tests/expiry.test.ts` 2. Alarm before the cliff, pause the workflow at it One alarm beats forty identical run failures. Files: `src/credentials/watch.ts` 3. Write down the publishing status decision Verification or weekly reauthorisation. Both are fine; drifting is not. Files: `docs/oauth-decision.md` ### Done when - [ ] No credential row has a null expiry - [ ] The warning window raises an alarm - [ ] A refresh failure pauses rather than fails runs - [ ] The publishing status decision is documented ### Watch out - A personal automation against a large provider usually runs on an application whose consent screen is still in testing, and that provider's own documentation says a project with an external user type and a publishing status of testing is issued a refresh token that expires in seven days unless the scopes are a subset of name, email and profile, so your automation dies every single week for reasons that look like your code; store the expiry, alarm on it, and decide deliberately whether to go through verification or to accept a weekly reauthorisation and automate the reminder. ## M5 · Reading a run, replaying it, stopping it (production only) Any failed run can be understood from its timeline and safely re-run, and the whole thing can be stopped in one action. ### Steps 1. Make dry run the default for every replay The debug path must not be the destructive path. Files: `src/runs/replay.ts`, `tests/replay-dry.test.ts` 2. Reuse the original idempotency keys on a real replay Skipping what already happened is the point. Files: `src/runs/replay.ts` 3. Add a kill switch and a spend ceiling Both are things you will want at two in the morning. Files: `src/worker/control.ts`, `src/lib/cost.ts` ### Done when - [ ] Dry run makes no external calls - [ ] A real replay skips completed side effects - [ ] The kill switch stops new claims quickly - [ ] The spend ceiling pauses without losing triggers ### Watch out - A replay button that simply re-runs the workflow performs every side effect again, so debugging a failed run by replaying it sends the customer a second email and creates a second record; make dry run the default, reuse the original idempotency keys on a real replay so anything already done is skipped, and show a diff of what would change before anything runs. ===== OPERATIONS.md ===== # Operations · Lindy ## Backup Postgres nightly and before every schema change. Redis is not backed up on purpose: it holds queued work, and a lost queue is recoverable from the run rows while a lost ledger means you cannot tell what already happened. ## Restore Restore Postgres, start the worker with the kill switch on, confirm the side_effects unique index exists, then release. Restoring without that index turns every subsequent retry into a duplicate, so check for it by name rather than trusting the dump. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Dead letters, runs stuck in running past a threshold, credentials inside the warning window, and monthly model spend. A run stuck in running is usually a stalled job being redelivered, which is the exact case the ledger exists for. ## Incident checklist The incident is a side effect that happened twice or a run that stopped halfway. Start from the run timeline, then the ledger for what actually reached the outside world, then the dead letters. Fix forward with a keyed replay rather than by re-triggering, because re-triggering makes a new run with new keys. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Three executions of a step are confirmed to produce one external call - [ ] A worker kill mid-step is confirmed not to repeat the call - [ ] Unvalidated model text is confirmed never to become a step output - [ ] No credential row is confirmed to have a null expiry - [ ] Replay is confirmed to default to dry run and make no calls ## Launch constraint Do not market omitted Lindy capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Runs, steps and the side-effect ledger. This is the one that must not be lost. DATABASE_URL=postgres://localhost:5432/automation # Required · secret. The queue. Losing it loses queued work, not history. REDIS_URL=redis://localhost:6379 # Required · secret. Encrypts connector tokens at rest. Rotating it means re-encrypting every row. CREDENTIAL_ENC_KEY=base64:...redacted # Required · secret. Verified against the raw body, not the parsed object. WEBHOOK_SIGNING_SECRET=whsec_...redacted # Required. Payloads outside this window are rejected as stale. WEBHOOK_TOLERANCE_SECONDS=300 # Required. How early a dying connector token raises an alarm. TOKEN_WARN_DAYS=2 # Required. Model spend for the month. At the ceiling the workflow pauses. MONTHLY_COST_CEILING=10.00 # Required. Set to on and the worker stops claiming jobs within seconds. KILL_SWITCH=off
# Lindy · indie build A single-workflow automation runner whose hard problems are surviving at-least-once delivery without repeating a side effect, keeping connector credentials alive past the week they were issued, and making a failed run readable enough to replay rather than guess at. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A Node worker process plus a small web interface over Postgres | the interface exists to read runs; the worker is the product | | Queue | Redis-backed jobs with an explicit idempotency key written per side effect | a queue redelivers, so every step has to survive being run twice | | Steps | Typed step definitions with a validated input and output schema | untyped blobs passed between steps fail at step four with a stack trace and no idea why | | Credentials | OAuth tokens encrypted at rest with the expiry stored beside them | your automation will break on a token long before it breaks on your code | | Model step | One call, structured output, schema-checked, never free text into the next step | a paragraph where the next step expected an identifier is the usual cause of death | | Replay | The trigger payload stored verbatim so a run can be repeated without the trigger | you cannot debug what you cannot re-run | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the worker and the interface. - Get it: nodejs.org or a version manager. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL and Redis** · free locally, a few pounds a month hosted - Why: Postgres holds runs and the side-effect ledger, Redis holds the queue, and the separation is deliberate because one of them must survive the other. - Get it: Docker Compose locally, or a small managed pair. - Verify: A job enqueued in Redis produces a run row in Postgres and survives a Redis restart - [ ] **API credentials for the two services in your workflow** · free - Why: The whole build is shaped by how those two authenticate, and finding out that one of them issues short-lived tokens after you have written the worker is the expensive order to do this in. - Get it: Register an application with each and record the token lifetime from their own documentation. - Verify: A token is issued and its expires_at is stored - [ ] **Your OAuth consent screen publishing status** · free, though verification costs time - Why: A consent screen left in testing can mean a refresh token that expires within days, which turns a working automation into a weekly chore, and the alternative is a verification process with its own cost in time. - Get it: Read the provider's current documentation on refresh token expiry for testing projects and choose deliberately. - Verify: The choice and its consequence are written in the README - [ ] **A recorded trigger payload from each service** · free - Why: Dry runs, replay tests and the whole test suite need a real payload, and capturing one before you build is much easier than reproducing one afterwards. - Get it: Point the webhook at a request bin once and save the raw body. - Verify: The suite runs end to end from the fixture with no network access ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - A library of maintained connectors. - Bulk email from your own domain. - A visual workflow builder. - Multi-tenant credentials and approval chains. - schema drift handling and enterprise controls - sender reputation, deliverability, and suppression handling - hundreds of maintained connectors - OAuth app verification - durable execution at scale If one of those is essential to you, that is the reason to keep paying for Lindy, and the README should say so rather than pretend.
# Build brief · Lindy The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me one automation you actually run, with a stored trigger, typed steps, retries that cannot double-charge you and a replay button to replace Lindy. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: A Node worker process plus a small web interface over Postgres · the interface exists to read runs; the worker is the product - Queue: Redis-backed jobs with an explicit idempotency key written per side effect · a queue redelivers, so every step has to survive being run twice - Steps: Typed step definitions with a validated input and output schema · untyped blobs passed between steps fail at step four with a stack trace and no idea why - Credentials: OAuth tokens encrypted at rest with the expiry stored beside them · your automation will break on a token long before it breaks on your code - Model step: One call, structured output, schema-checked, never free text into the next step · a paragraph where the next step expected an identifier is the usual cause of death - Replay: The trigger payload stored verbatim so a run can be repeated without the trigger · you cannot debug what you cannot re-run ### Data model (create this before Phase 1) - workflows(id, name, version, trigger_kind, trigger_config_json, enabled, updated_at) - runs(id, workflow_id, workflow_version, trigger_payload_json, started_at, ended_at, state, replay_of_run_id, dry_run) - steps(id, run_id, ordinal, step_type, input_json, output_json, state, attempts, started_at, ended_at, error_json) - side_effects(id, run_id, step_id, idempotency_key, external_ref, at) · unique on idempotency_key, and the only record that something already happened out in the world - credentials(id, provider, account, access_token_enc, refresh_token_enc, expires_at, scopes_json, last_refreshed_at, status) - dead_letters(id, run_id, step_id, at, reason, payload_json) · where a run goes when retrying would be guessing - workflow_version is copied into every run. Editing a workflow must never change what an old run says it did, which is why the definition is versioned rather than updated in place. ### Phase 1 · One trigger, one run, kept verbatim Build: Support one webhook trigger and one schedule trigger. On receipt, write the run with the raw payload and the current workflow version, then enqueue it. Verify webhook signatures where the sender provides them, and reject a payload whose timestamp is outside a stated tolerance. Trap: Parsing the webhook body and storing the parsed object loses the raw bytes you need to verify the signature, and signature verification against a re-serialised body fails for reasons that take an evening to find; keep the raw body, verify against it, and store it as received so a replay months later feeds the step exactly what the step saw the first time. Done when: The raw request body is stored byte-for-byte and the signature is verified against those bytes, tested with a deliberately re-serialised body that must fail. A payload older than the configured tolerance is rejected with a reason recorded. Every run carries the workflow version that was current when it started, and editing the workflow afterwards does not change it, asserted in a test. A trigger that arrives while the workflow is disabled is recorded and not executed. Do not build yet: No steps, retries, credentials or model calls yet. ### Phase 2 · Steps that survive being run twice Build: Give each step an idempotency key derived from the run id, the step ordinal and a stable hash of its input. Before performing a side effect, look for that key in side_effects. Write the key and the external reference in the same transaction that records the step output. Trap: A worker that loses its lock while busy has its job marked stalled, and the documented behaviour is that the job is moved back to waiting and processed again by another worker, so a step that sends an email or creates an invoice does it twice while your code never saw an error; check for the idempotency key first, write it with the result in one transaction, and confirm the default stall settings in the queue library you chose rather than assuming they give you room. Done when: Running the same step three times produces one row in side_effects and one external call, tested with a stub that counts calls. Killing the worker between the external call and the transaction leaves the run resumable and does not repeat the call, tested by killing at that point. The unique index on idempotency_key exists and a duplicate insert raises rather than being swallowed. A step whose input changes produces a different key, tested. Do not build yet: No model step, credentials or replay yet. ### Phase 3 · The model step, typed on both sides Build: Define the model step with an input schema and an output schema. Call once with structured output, validate, and on a validation failure retry once with the error attached, then fail the run. Store the raw response, the model identifier and the prompt version on the step row. Trap: Letting a model step return a paragraph and passing it to the next step means the automation fails somewhere downstream with a message about an undefined property, and because the model usually returns something reasonable it will pass your testing and break in week three; validate against the schema at the step boundary, treat a validation miss as its own failure kind, and never let unvalidated text become a step output. Done when: A model response that does not validate never becomes a step output, tested with a stub returning prose. The model identifier and prompt version are stored on every step that used a model, so a changed answer can be attributed. A validation failure retries exactly once and then fails the run with the raw response kept. A model step is skippable in dry run and the rest of the workflow still executes against a recorded fixture. Do not build yet: No credential lifecycle, replay or kill switch yet. ### Phase 4 · Credentials, and the seven day cliff Build: Store expires_at and scopes with every token, refresh ahead of expiry, and record every refresh. Alarm when a token is inside the warning window or when a refresh fails. Put the consent screen's publishing status in the README as a decision, not a footnote. Trap: A personal automation against a large provider usually runs on an application whose consent screen is still in testing, and that provider's own documentation says a project with an external user type and a publishing status of testing is issued a refresh token that expires in seven days unless the scopes are a subset of name, email and profile, so your automation dies every single week for reasons that look like your code; store the expiry, alarm on it, and decide deliberately whether to go through verification or to accept a weekly reauthorisation and automate the reminder. Done when: Every credential row has expires_at populated at issue time, asserted by a test that rejects a null. A credential inside the warning window raises a visible alarm and a log entry, tested by moving the clock. A refresh failure marks the credential and pauses the workflows that depend on it rather than failing their runs one by one. The README states the consent screen publishing status, what it implies for token lifetime, and which route you chose. Do not build yet: No replay, dry run or cost ceilings yet. ### Phase 5 · Reading a run, replaying it, stopping it Build: A run timeline showing each step with its input, output, attempts and error. A replay that defaults to dry run and shows what each step would do. A real replay reuses the original idempotency keys. A global kill switch, a per-workflow concurrency of one, and a spend ceiling. Trap: A replay button that simply re-runs the workflow performs every side effect again, so debugging a failed run by replaying it sends the customer a second email and creates a second record; make dry run the default, reuse the original idempotency keys on a real replay so anything already done is skipped, and show a diff of what would change before anything runs. Done when: Replay defaults to dry run and a dry run makes no external call, asserted with a stub that fails the test if called. A real replay reuses the original idempotency keys and skips completed side effects, tested against a half-finished run. The kill switch stops the worker claiming new jobs within a stated number of seconds and lets in-flight steps finish. Reaching the spend or call ceiling pauses the workflow with a message and does not lose queued triggers. Do not build yet: Nothing after this. ### Out of scope (and why) - A connector library. This build supports the two services you actually use, because maintaining hundreds of integrations against changing APIs is the subscription rather than the code. - Sending bulk email from your own domain, which is a deliverability and reputation problem that will cost you more attention than the automation saves. - A visual workflow builder, which is a large interface project that adds nothing to a workflow you can define in one typed file. - Multi-tenant credentials and an approval chain, which turn a personal tool into something that needs an audit story and a security review. ### README must contain - That the queue delivers at least once, and that the idempotency key in side_effects is what makes that safe - That runs pin the workflow version, so editing a workflow never rewrites what an old run reports - That connector tokens expire, what your consent screen's publishing status implies for that, and which route you chose - That replay defaults to dry run and how a real replay reuses the original keys - Where credentials are encrypted, how to rotate the encryption key, and how to revoke one connector
# Agent instructions · Lindy indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A Node worker process plus a small web interface over Postgres, Redis-backed jobs with an explicit idempotency key written per side effect, Typed step definitions with a validated input and output schema, OAuth tokens encrypted at rest with the expiry stored beside them, One call, structured output, schema-checked, never free text into the next step, The trigger payload stored verbatim so a run can be repeated without the trigger. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Parsing the webhook body and storing the parsed object loses the raw bytes you need to verify the signature, and signature verification against a re-serialised body fails for reasons that take an evening to find; keep the raw body, verify against it, and store it as received so a replay months later feeds the step exactly what the step saw the first time. - A worker that loses its lock while busy has its job marked stalled, and the documented behaviour is that the job is moved back to waiting and processed again by another worker, so a step that sends an email or creates an invoice does it twice while your code never saw an error; check for the idempotency key first, write it with the result in one transaction, and confirm the default stall settings in the queue library you chose rather than assuming they give you room. - Letting a model step return a paragraph and passing it to the next step means the automation fails somewhere downstream with a message about an undefined property, and because the model usually returns something reasonable it will pass your testing and break in week three; validate against the schema at the step boundary, treat a validation miss as its own failure kind, and never let unvalidated text become a step output. - A personal automation against a large provider usually runs on an application whose consent screen is still in testing, and that provider's own documentation says a project with an external user type and a publishing status of testing is issued a refresh token that expires in seven days unless the scopes are a subset of name, email and profile, so your automation dies every single week for reasons that look like your code; store the expiry, alarm on it, and decide deliberately whether to go through verification or to accept a weekly reauthorisation and automate the reminder.
# Build plan · Lindy A single-workflow automation runner whose hard problems are surviving at-least-once delivery without repeating a side effect, keeping connector credentials alive past the week they were issued, and making a failed run readable enough to replay rather than guess at. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · One trigger, one run, kept verbatim A trigger creates a run row holding the exact payload that caused it, before any step executes. ### Steps 1. Store the raw body before parsing anything Signature verification and replay both need the original bytes. Files: `src/triggers/webhook.ts`, `tests/signature.test.ts` 2. Copy the workflow version into the run An old run must keep describing what it actually did. Files: `src/db/schema.sql`, `src/runs/create.ts` 3. Reject stale payloads with a recorded reason A replayed webhook from last month is an attack, not a trigger. Files: `src/triggers/webhook.ts` ### Done when - [ ] The raw body is stored and verified byte-for-byte - [ ] Stale payloads are rejected with a reason - [ ] Run rows pin the workflow version - [ ] Triggers on a disabled workflow are recorded, not run ### Watch out - Parsing the webhook body and storing the parsed object loses the raw bytes you need to verify the signature, and signature verification against a re-serialised body fails for reasons that take an evening to find; keep the raw body, verify against it, and store it as received so a replay months later feeds the step exactly what the step saw the first time. ## Phase 2 · Steps that survive being run twice Every side effect happens exactly once even when the queue delivers the job three times. ### Steps 1. Derive the key from run, ordinal and input hash Stable across retries, different across inputs. Files: `src/steps/idempotency.ts`, `tests/idempotency.test.ts` 2. Check the key, then act, then write in one transaction The order is the whole mechanism. Files: `src/steps/run-step.ts` 3. Read your queue's stall and retry defaults and write them down You are going to be redelivered whether you planned for it or not. Files: `docs/queue-behaviour.md` ### Done when - [ ] Three executions produce one external call - [ ] A kill mid-step never repeats the call - [ ] Duplicate idempotency keys raise - [ ] A changed input yields a different key ### Watch out - A worker that loses its lock while busy has its job marked stalled, and the documented behaviour is that the job is moved back to waiting and processed again by another worker, so a step that sends an email or creates an invoice does it twice while your code never saw an error; check for the idempotency key first, write it with the result in one transaction, and confirm the default stall settings in the queue library you chose rather than assuming they give you room. ## Phase 3 · The model step, typed on both sides A model call either produces schema-valid structured data or stops the run. ### Steps 1. Schema-check the output at the step boundary The boundary is the only place this is cheap to catch. Files: `src/steps/model.ts`, `tests/model-schema.test.ts` 2. Store model id and prompt version on the step So a different answer next month has an explanation. Files: `src/db/schema.sql` 3. Retry once with the validation error attached, then stop A third attempt at the same bad shape is just money. Files: `src/steps/model.ts` ### Done when - [ ] Unvalidated text never becomes a step output - [ ] Model id and prompt version are recorded - [ ] Validation failure retries once and then fails - [ ] Dry run substitutes a recorded fixture ### Watch out - Letting a model step return a paragraph and passing it to the next step means the automation fails somewhere downstream with a message about an undefined property, and because the model usually returns something reasonable it will pass your testing and break in week three; validate against the schema at the step boundary, treat a validation miss as its own failure kind, and never let unvalidated text become a step output. ## Phase 4 · Credentials, and the seven day cliff A connector token's expiry is known in advance and never discovered at run time. ### Steps 1. Record expires_at and scopes at issue time A token with an unknown expiry is a token that will surprise you. Files: `src/credentials/store.ts`, `tests/expiry.test.ts` 2. Alarm before the cliff, pause the workflow at it One alarm beats forty identical run failures. Files: `src/credentials/watch.ts` 3. Write down the publishing status decision Verification or weekly reauthorisation. Both are fine; drifting is not. Files: `docs/oauth-decision.md` ### Done when - [ ] No credential row has a null expiry - [ ] The warning window raises an alarm - [ ] A refresh failure pauses rather than fails runs - [ ] The publishing status decision is documented ### Watch out - A personal automation against a large provider usually runs on an application whose consent screen is still in testing, and that provider's own documentation says a project with an external user type and a publishing status of testing is issued a refresh token that expires in seven days unless the scopes are a subset of name, email and profile, so your automation dies every single week for reasons that look like your code; store the expiry, alarm on it, and decide deliberately whether to go through verification or to accept a weekly reauthorisation and automate the reminder. ## Not in this build - A library of maintained connectors. - Bulk email from your own domain. - A visual workflow builder. - Multi-tenant credentials and approval chains. ## After v1, if you want it - A second workflow, which is the only real test of whether your step types were general or accidental - A weekly digest of runs, failures and spend, built from the tables you already have - Recorded fixtures captured automatically from production runs, so the test suite grows from real payloads
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Runs, steps and the side-effect ledger. This is the one that must not be lost. DATABASE_URL=postgres://localhost:5432/automation # Required · secret. The queue. Losing it loses queued work, not history. REDIS_URL=redis://localhost:6379 # Required · secret. Encrypts connector tokens at rest. Rotating it means re-encrypting every row. CREDENTIAL_ENC_KEY=base64:...redacted # Required · secret. Verified against the raw body, not the parsed object. WEBHOOK_SIGNING_SECRET=whsec_...redacted # Required. Payloads outside this window are rejected as stale. WEBHOOK_TOLERANCE_SECONDS=300 # Required. How early a dying connector token raises an alarm. TOKEN_WARN_DAYS=2 # Required. Model spend for the month. At the ceiling the workflow pauses. MONTHLY_COST_CEILING=10.00 # Required. Set to on and the worker stops claiming jobs within seconds. KILL_SWITCH=off
# Lindy · product brief ## Problem The visible AI assistants + automation loop is buildable, but a credible replacement needs more than the first screen. Lindy earns its keep through connectors, auth, reliability, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome One automation that records exactly what triggered it, performs each side effect once whatever the queue does, tells you a week before its credentials expire, and can be replayed as a dry run before it is replayed for real. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - hosted PostgreSQL and Redis - API credentials for the two chosen services - Node.js 22 ## Explicit non-goals for v1 - A library of maintained connectors. - Bulk email from your own domain. - A visual workflow builder. - Multi-tenant credentials and approval chains. - schema drift handling and enterprise controls - sender reputation, deliverability, and suppression handling - hundreds of maintained connectors - OAuth app verification - durable execution at scale ## Success criteria - Three executions of a step are confirmed to produce one external call - A worker kill mid-step is confirmed not to repeat the call - Unvalidated model text is confirmed never to become a step output - No credential row is confirmed to have a null expiry - Replay is confirmed to default to dry run and make no calls
# Build brief · Lindy The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me one automation you actually run, with a stored trigger, typed steps, retries that cannot double-charge you and a replay button to replace Lindy. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: A Node worker process plus a small web interface over Postgres · the interface exists to read runs; the worker is the product - Queue: Redis-backed jobs with an explicit idempotency key written per side effect · a queue redelivers, so every step has to survive being run twice - Steps: Typed step definitions with a validated input and output schema · untyped blobs passed between steps fail at step four with a stack trace and no idea why - Credentials: OAuth tokens encrypted at rest with the expiry stored beside them · your automation will break on a token long before it breaks on your code - Model step: One call, structured output, schema-checked, never free text into the next step · a paragraph where the next step expected an identifier is the usual cause of death - Replay: The trigger payload stored verbatim so a run can be repeated without the trigger · you cannot debug what you cannot re-run ### Data model (create this before Phase 1) - workflows(id, name, version, trigger_kind, trigger_config_json, enabled, updated_at) - runs(id, workflow_id, workflow_version, trigger_payload_json, started_at, ended_at, state, replay_of_run_id, dry_run) - steps(id, run_id, ordinal, step_type, input_json, output_json, state, attempts, started_at, ended_at, error_json) - side_effects(id, run_id, step_id, idempotency_key, external_ref, at) · unique on idempotency_key, and the only record that something already happened out in the world - credentials(id, provider, account, access_token_enc, refresh_token_enc, expires_at, scopes_json, last_refreshed_at, status) - dead_letters(id, run_id, step_id, at, reason, payload_json) · where a run goes when retrying would be guessing - workflow_version is copied into every run. Editing a workflow must never change what an old run says it did, which is why the definition is versioned rather than updated in place. ### Phase 1 · One trigger, one run, kept verbatim Build: Support one webhook trigger and one schedule trigger. On receipt, write the run with the raw payload and the current workflow version, then enqueue it. Verify webhook signatures where the sender provides them, and reject a payload whose timestamp is outside a stated tolerance. Trap: Parsing the webhook body and storing the parsed object loses the raw bytes you need to verify the signature, and signature verification against a re-serialised body fails for reasons that take an evening to find; keep the raw body, verify against it, and store it as received so a replay months later feeds the step exactly what the step saw the first time. Done when: The raw request body is stored byte-for-byte and the signature is verified against those bytes, tested with a deliberately re-serialised body that must fail. A payload older than the configured tolerance is rejected with a reason recorded. Every run carries the workflow version that was current when it started, and editing the workflow afterwards does not change it, asserted in a test. A trigger that arrives while the workflow is disabled is recorded and not executed. Do not build yet: No steps, retries, credentials or model calls yet. ### Phase 2 · Steps that survive being run twice Build: Give each step an idempotency key derived from the run id, the step ordinal and a stable hash of its input. Before performing a side effect, look for that key in side_effects. Write the key and the external reference in the same transaction that records the step output. Trap: A worker that loses its lock while busy has its job marked stalled, and the documented behaviour is that the job is moved back to waiting and processed again by another worker, so a step that sends an email or creates an invoice does it twice while your code never saw an error; check for the idempotency key first, write it with the result in one transaction, and confirm the default stall settings in the queue library you chose rather than assuming they give you room. Done when: Running the same step three times produces one row in side_effects and one external call, tested with a stub that counts calls. Killing the worker between the external call and the transaction leaves the run resumable and does not repeat the call, tested by killing at that point. The unique index on idempotency_key exists and a duplicate insert raises rather than being swallowed. A step whose input changes produces a different key, tested. Do not build yet: No model step, credentials or replay yet. ### Phase 3 · The model step, typed on both sides Build: Define the model step with an input schema and an output schema. Call once with structured output, validate, and on a validation failure retry once with the error attached, then fail the run. Store the raw response, the model identifier and the prompt version on the step row. Trap: Letting a model step return a paragraph and passing it to the next step means the automation fails somewhere downstream with a message about an undefined property, and because the model usually returns something reasonable it will pass your testing and break in week three; validate against the schema at the step boundary, treat a validation miss as its own failure kind, and never let unvalidated text become a step output. Done when: A model response that does not validate never becomes a step output, tested with a stub returning prose. The model identifier and prompt version are stored on every step that used a model, so a changed answer can be attributed. A validation failure retries exactly once and then fails the run with the raw response kept. A model step is skippable in dry run and the rest of the workflow still executes against a recorded fixture. Do not build yet: No credential lifecycle, replay or kill switch yet. ### Phase 4 · Credentials, and the seven day cliff Build: Store expires_at and scopes with every token, refresh ahead of expiry, and record every refresh. Alarm when a token is inside the warning window or when a refresh fails. Put the consent screen's publishing status in the README as a decision, not a footnote. Trap: A personal automation against a large provider usually runs on an application whose consent screen is still in testing, and that provider's own documentation says a project with an external user type and a publishing status of testing is issued a refresh token that expires in seven days unless the scopes are a subset of name, email and profile, so your automation dies every single week for reasons that look like your code; store the expiry, alarm on it, and decide deliberately whether to go through verification or to accept a weekly reauthorisation and automate the reminder. Done when: Every credential row has expires_at populated at issue time, asserted by a test that rejects a null. A credential inside the warning window raises a visible alarm and a log entry, tested by moving the clock. A refresh failure marks the credential and pauses the workflows that depend on it rather than failing their runs one by one. The README states the consent screen publishing status, what it implies for token lifetime, and which route you chose. Do not build yet: No replay, dry run or cost ceilings yet. ### Phase 5 · Reading a run, replaying it, stopping it Build: A run timeline showing each step with its input, output, attempts and error. A replay that defaults to dry run and shows what each step would do. A real replay reuses the original idempotency keys. A global kill switch, a per-workflow concurrency of one, and a spend ceiling. Trap: A replay button that simply re-runs the workflow performs every side effect again, so debugging a failed run by replaying it sends the customer a second email and creates a second record; make dry run the default, reuse the original idempotency keys on a real replay so anything already done is skipped, and show a diff of what would change before anything runs. Done when: Replay defaults to dry run and a dry run makes no external call, asserted with a stub that fails the test if called. A real replay reuses the original idempotency keys and skips completed side effects, tested against a half-finished run. The kill switch stops the worker claiming new jobs within a stated number of seconds and lets in-flight steps finish. Reaching the spend or call ceiling pauses the workflow with a message and does not lose queued triggers. Do not build yet: Nothing after this. ### Out of scope (and why) - A connector library. This build supports the two services you actually use, because maintaining hundreds of integrations against changing APIs is the subscription rather than the code. - Sending bulk email from your own domain, which is a deliverability and reputation problem that will cost you more attention than the automation saves. - A visual workflow builder, which is a large interface project that adds nothing to a workflow you can define in one typed file. - Multi-tenant credentials and an approval chain, which turn a personal tool into something that needs an audit story and a security review. ### README must contain - That the queue delivers at least once, and that the idempotency key in side_effects is what makes that safe - That runs pin the workflow version, so editing a workflow never rewrites what an old run reports - That connector tokens expire, what your consent screen's publishing status implies for that, and which route you chose - That replay defaults to dry run and how a real replay reuses the original keys - Where credentials are encrypted, how to rotate the encryption key, and how to revoke one connector
# Architecture · Lindy ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A Node worker process plus a small web interface over Postgres | the interface exists to read runs; the worker is the product | | Queue | Redis-backed jobs with an explicit idempotency key written per side effect | a queue redelivers, so every step has to survive being run twice | | Steps | Typed step definitions with a validated input and output schema | untyped blobs passed between steps fail at step four with a stack trace and no idea why | | Credentials | OAuth tokens encrypted at rest with the expiry stored beside them | your automation will break on a token long before it breaks on your code | | Model step | One call, structured output, schema-checked, never free text into the next step | a paragraph where the next step expected an identifier is the usual cause of death | | Replay | The trigger payload stored verbatim so a run can be repeated without the trigger | you cannot debug what you cannot re-run | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Triggers | Webhook and schedule intake, raw payload storage. | Any trigger, provided the raw bytes are stored before parsing. | | Engine | Step execution, attempts, states, dead letters. | Any executor, provided every side effect is keyed. | | Ledger | side_effects and its unique index. | Not swappable. This table is the reason retries are safe. | | Credentials | Encrypted tokens, refresh, expiry alarms. | Any secret store, provided expiry is recorded at issue. | | Console | Run timelines, replay, kill switch, ceilings. | Any interface, provided replay defaults to dry run. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Runs, steps and the side-effect ledger. This is the one that must not be lost. - `REDIS_URL` · required, secret · The queue. Losing it loses queued work, not history. - `CREDENTIAL_ENC_KEY` · required, secret · Encrypts connector tokens at rest. Rotating it means re-encrypting every row. - `WEBHOOK_SIGNING_SECRET` · required, secret · Verified against the raw body, not the parsed object. - `WEBHOOK_TOLERANCE_SECONDS` · required · Payloads outside this window are rejected as stale. - `TOKEN_WARN_DAYS` · required · How early a dying connector token raises an alarm. - `MONTHLY_COST_CEILING` · required · Model spend for the month. At the ceiling the workflow pauses. - `KILL_SWITCH` · required · Set to on and the worker stops claiming jobs within seconds. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Lindy product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: A Node worker process plus a small web interface over Postgres, Redis-backed jobs with an explicit idempotency key written per side effect, Typed step definitions with a validated input and output schema, OAuth tokens encrypted at rest with the expiry stored beside them, One call, structured output, schema-checked, never free text into the next step, The trigger payload stored verbatim so a run can be repeated without the trigger. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Parsing the webhook body and storing the parsed object loses the raw bytes you need to verify the signature, and signature verification against a re-serialised body fails for reasons that take an evening to find; keep the raw body, verify against it, and store it as received so a replay months later feeds the step exactly what the step saw the first time. - A worker that loses its lock while busy has its job marked stalled, and the documented behaviour is that the job is moved back to waiting and processed again by another worker, so a step that sends an email or creates an invoice does it twice while your code never saw an error; check for the idempotency key first, write it with the result in one transaction, and confirm the default stall settings in the queue library you chose rather than assuming they give you room. - Letting a model step return a paragraph and passing it to the next step means the automation fails somewhere downstream with a message about an undefined property, and because the model usually returns something reasonable it will pass your testing and break in week three; validate against the schema at the step boundary, treat a validation miss as its own failure kind, and never let unvalidated text become a step output. - A personal automation against a large provider usually runs on an application whose consent screen is still in testing, and that provider's own documentation says a project with an external user type and a publishing status of testing is issued a refresh token that expires in seven days unless the scopes are a subset of name, email and profile, so your automation dies every single week for reasons that look like your code; store the expiry, alarm on it, and decide deliberately whether to go through verification or to accept a weekly reauthorisation and automate the reminder. - A replay button that simply re-runs the workflow performs every side effect again, so debugging a failed run by replaying it sends the customer a second email and creates a second record; make dry run the default, reuse the original idempotency keys on a real replay so anything already done is skipped, and show a diff of what would change before anything runs.
# Delivery milestones · Lindy Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · One trigger, one run, kept verbatim A trigger creates a run row holding the exact payload that caused it, before any step executes. ### Steps 1. Store the raw body before parsing anything Signature verification and replay both need the original bytes. Files: `src/triggers/webhook.ts`, `tests/signature.test.ts` 2. Copy the workflow version into the run An old run must keep describing what it actually did. Files: `src/db/schema.sql`, `src/runs/create.ts` 3. Reject stale payloads with a recorded reason A replayed webhook from last month is an attack, not a trigger. Files: `src/triggers/webhook.ts` ### Done when - [ ] The raw body is stored and verified byte-for-byte - [ ] Stale payloads are rejected with a reason - [ ] Run rows pin the workflow version - [ ] Triggers on a disabled workflow are recorded, not run ### Watch out - Parsing the webhook body and storing the parsed object loses the raw bytes you need to verify the signature, and signature verification against a re-serialised body fails for reasons that take an evening to find; keep the raw body, verify against it, and store it as received so a replay months later feeds the step exactly what the step saw the first time. ## M2 · Steps that survive being run twice Every side effect happens exactly once even when the queue delivers the job three times. ### Steps 1. Derive the key from run, ordinal and input hash Stable across retries, different across inputs. Files: `src/steps/idempotency.ts`, `tests/idempotency.test.ts` 2. Check the key, then act, then write in one transaction The order is the whole mechanism. Files: `src/steps/run-step.ts` 3. Read your queue's stall and retry defaults and write them down You are going to be redelivered whether you planned for it or not. Files: `docs/queue-behaviour.md` ### Done when - [ ] Three executions produce one external call - [ ] A kill mid-step never repeats the call - [ ] Duplicate idempotency keys raise - [ ] A changed input yields a different key ### Watch out - A worker that loses its lock while busy has its job marked stalled, and the documented behaviour is that the job is moved back to waiting and processed again by another worker, so a step that sends an email or creates an invoice does it twice while your code never saw an error; check for the idempotency key first, write it with the result in one transaction, and confirm the default stall settings in the queue library you chose rather than assuming they give you room. ## M3 · The model step, typed on both sides A model call either produces schema-valid structured data or stops the run. ### Steps 1. Schema-check the output at the step boundary The boundary is the only place this is cheap to catch. Files: `src/steps/model.ts`, `tests/model-schema.test.ts` 2. Store model id and prompt version on the step So a different answer next month has an explanation. Files: `src/db/schema.sql` 3. Retry once with the validation error attached, then stop A third attempt at the same bad shape is just money. Files: `src/steps/model.ts` ### Done when - [ ] Unvalidated text never becomes a step output - [ ] Model id and prompt version are recorded - [ ] Validation failure retries once and then fails - [ ] Dry run substitutes a recorded fixture ### Watch out - Letting a model step return a paragraph and passing it to the next step means the automation fails somewhere downstream with a message about an undefined property, and because the model usually returns something reasonable it will pass your testing and break in week three; validate against the schema at the step boundary, treat a validation miss as its own failure kind, and never let unvalidated text become a step output. ## M4 · Credentials, and the seven day cliff A connector token's expiry is known in advance and never discovered at run time. ### Steps 1. Record expires_at and scopes at issue time A token with an unknown expiry is a token that will surprise you. Files: `src/credentials/store.ts`, `tests/expiry.test.ts` 2. Alarm before the cliff, pause the workflow at it One alarm beats forty identical run failures. Files: `src/credentials/watch.ts` 3. Write down the publishing status decision Verification or weekly reauthorisation. Both are fine; drifting is not. Files: `docs/oauth-decision.md` ### Done when - [ ] No credential row has a null expiry - [ ] The warning window raises an alarm - [ ] A refresh failure pauses rather than fails runs - [ ] The publishing status decision is documented ### Watch out - A personal automation against a large provider usually runs on an application whose consent screen is still in testing, and that provider's own documentation says a project with an external user type and a publishing status of testing is issued a refresh token that expires in seven days unless the scopes are a subset of name, email and profile, so your automation dies every single week for reasons that look like your code; store the expiry, alarm on it, and decide deliberately whether to go through verification or to accept a weekly reauthorisation and automate the reminder. ## M5 · Reading a run, replaying it, stopping it (production only) Any failed run can be understood from its timeline and safely re-run, and the whole thing can be stopped in one action. ### Steps 1. Make dry run the default for every replay The debug path must not be the destructive path. Files: `src/runs/replay.ts`, `tests/replay-dry.test.ts` 2. Reuse the original idempotency keys on a real replay Skipping what already happened is the point. Files: `src/runs/replay.ts` 3. Add a kill switch and a spend ceiling Both are things you will want at two in the morning. Files: `src/worker/control.ts`, `src/lib/cost.ts` ### Done when - [ ] Dry run makes no external calls - [ ] A real replay skips completed side effects - [ ] The kill switch stops new claims quickly - [ ] The spend ceiling pauses without losing triggers ### Watch out - A replay button that simply re-runs the workflow performs every side effect again, so debugging a failed run by replaying it sends the customer a second email and creates a second record; make dry run the default, reuse the original idempotency keys on a real replay so anything already done is skipped, and show a diff of what would change before anything runs.
# Operations · Lindy ## Backup Postgres nightly and before every schema change. Redis is not backed up on purpose: it holds queued work, and a lost queue is recoverable from the run rows while a lost ledger means you cannot tell what already happened. ## Restore Restore Postgres, start the worker with the kill switch on, confirm the side_effects unique index exists, then release. Restoring without that index turns every subsequent retry into a duplicate, so check for it by name rather than trusting the dump. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Dead letters, runs stuck in running past a threshold, credentials inside the warning window, and monthly model spend. A run stuck in running is usually a stalled job being redelivered, which is the exact case the ledger exists for. ## Incident checklist The incident is a side effect that happened twice or a run that stopped halfway. Start from the run timeline, then the ledger for what actually reached the outside world, then the dead letters. Fix forward with a keyed replay rather than by re-triggering, because re-triggering makes a new run with new keys. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Three executions of a step are confirmed to produce one external call - [ ] A worker kill mid-step is confirmed not to repeat the call - [ ] Unvalidated model text is confirmed never to become a step output - [ ] No credential row is confirmed to have a null expiry - [ ] Replay is confirmed to default to dry run and make no calls ## Launch constraint Do not market omitted Lindy capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Runs, steps and the side-effect ledger. This is the one that must not be lost. DATABASE_URL=postgres://localhost:5432/automation # Required · secret. The queue. Losing it loses queued work, not history. REDIS_URL=redis://localhost:6379 # Required · secret. Encrypts connector tokens at rest. Rotating it means re-encrypting every row. CREDENTIAL_ENC_KEY=base64:...redacted # Required · secret. Verified against the raw body, not the parsed object. WEBHOOK_SIGNING_SECRET=whsec_...redacted # Required. Payloads outside this window are rejected as stale. WEBHOOK_TOLERANCE_SECONDS=300 # Required. How early a dying connector token raises an alarm. TOKEN_WARN_DAYS=2 # Required. Model spend for the month. At the ceiling the workflow pauses. MONTHLY_COST_CEILING=10.00 # Required. Set to on and the worker stops claiming jobs within seconds. KILL_SWITCH=off
$ choose a build depth, inspect the files, then open the complete pack in your agent
Lindy: One automation is easy. Subscribers pay for maintained integrations, credentials, retries, observability, and someone else owning breakage.
xschema drift handling and enterprise controls
xsender reputation, deliverability, and suppression handling
xhundreds of maintained connectors
xOAuth app verification
xdurable execution at scale
Lindy pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| plus | $49.99/workspace | — | 2 connected inboxes; base monthly usage allowance is not stated numerically on the public card. |
| pro | $99.99/workspace | — | 3 connected inboxes; 3x the Plus usage allowance; absolute task or credit count not published. |
| max | $199.99/workspace | — | 5 connected inboxes; 7x the Plus usage allowance; absolute task or credit count not published. |
| enterprise | custom | — | Custom inbox, usage, security and support limits. |
free tierno permanent free tier; 7-day trial
billingmonthly only; no public annual plan
hidden costsUnused usage does not roll over. Optional overage is charged at 2x the standard credit cost; phone numbers cost $10/month each and US/Canada calls use about 20 credits per minute.
verified 2026-08-11 · source ↗
Vibecode Lindy
Kinda. The core of Lindy is buildable in a weekend with the prompt on this page, but there are real gaps: schema drift handling and enterprise controls, sender reputation, deliverability, and suppression handling. Read the honest list above before committing.
How much does Lindy cost?
Lindy's pricing is usage-based or varies by plan · The official pricing URL is recorded, but a stable current amount was not safely recoverable in this pass. Do not publish a number until rechecked..
What do I lose by replacing Lindy?
Honestly: schema drift handling and enterprise controls; sender reputation, deliverability, and suppression handling; hundreds of maintained connectors; OAuth app verification; durable execution at scale. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Lindy?
Yes: Activepieces (Open-source automation builder with connectors and self-hosting.), n8n (Source-available workflow automation engine and connector reference.). Using prior art is also vibecoding; the prompt is for when you want it exactly your way.