# barua: the agent pages in one file > barua's public Markdown pages for agents, joined into one file so a tool can read them in one fetch. The index is https://barua.ai/llms.txt. Each part starts with the address it was copied from. --- Source: https://barua.ai/llms.txt # barua > Headless email for AI agents: marketing and transactional email for product apps > and stores. A coding agent sets it up and maintains it with the barua-setup skill. The owner > chooses delegated permissions and optional review. Access is a conversation: chris@barua.ai. ## Choose one task Start with the [docs index](https://barua.ai/docs/index.md). Fetch only the relevant guide, then its required reference. Each HTML docs page links its Markdown source. Full request shapes live in the references; guides explain which one to use. - [Setup](https://barua.ai/docs/guides/quickstart.md): install and verify the integration. - [Keys and permissions](https://barua.ai/docs/guides/authentication.md): choose the correct credential. - [Owner access](https://barua.ai/docs/guides/owner-access.md): sign in and review decisions through the CLI. - [People](https://barua.ai/docs/guides/people.md): fetch profiles and activity. - [Segments and campaigns](https://barua.ai/docs/guides/audiences.md): select an audience and send an approved campaign. - [Event history](https://barua.ai/docs/guides/imports.md): backfill without triggering email. - [Plays](https://barua.ai/docs/guides/plays.md): operate automated email flows. - [Emails](https://barua.ai/docs/guides/emails.md): inspect history and recorded content. - [Workspace inspection](https://barua.ai/docs/guides/inspect-workspace.md): diagnose setup and missing results. - [Workspace closure](https://barua.ai/docs/guides/workspace-closure.md): close and export with an owner's explicit decision. - [Account deletion](https://barua.ai/docs/guides/account-deletion.md): check blockers and retain a deletion receipt. - [Recovery](https://barua.ai/docs/guides/troubleshooting.md): read current state before retrying. ## Start an integration Install with `npx skills add getbarua/skills --skill barua-setup`. Read the [setup skill](https://barua.ai/skills/barua-setup/SKILL.md) and its required branch references before acting. `BARUA_MANAGEMENT_KEY` authorizes owner-granted scopes. Read the [management API](https://barua.ai/skills/barua-setup/references/management.md) before changes. A test key for server events validates shapes without creating people or sending email; see [setup.md](https://barua.ai/setup.md). The [event contract](https://barua.ai/docs/events.md) defines server events. Integration discovery uses `GET /api/integrations/v1/plan`; verification uses `GET /api/integrations/v1/events/status`. [Installation and pixel](https://barua.ai/docs/install.md) holds the complete browser contract. ## Product and full export [Product overview](https://barua.ai/intro.md) · [Pricing](https://barua.ai/pricing.md) · [Request access](https://barua.ai/#access) · [Human docs](https://barua.ai/docs/) [llms-full.txt](https://barua.ai/llms-full.txt) is the optional complete Markdown export. It is large; use focused guides for routine tasks. --- Source: https://barua.ai/intro.md # Barua for agents Barua is a headless email service for AI agents. It writes and sends marketing and transactional emails in the product's voice, from its own domain. Your role is to understand the owner's product and customer journey, connect the relevant events, and manage email through Barua's API under the permissions the owner grants. The optional browser tag adds customer activity. Server events provide facts such as signups and paid orders. Barua supports automated email journeys and one-off campaigns to saved segments. It prepares emails using the workspace's brand and customer context. Exact versions are approved by the owner or a delegated agent before sending. The owner chooses permissions and review checkpoints; reading this page grants none. For a campaign, your agent selects a saved segment and supplies a brief. Barua prepares the email and fixes the audience for review. You approve that version before sending or scheduling it. Later permission checks can remove recipients; new segment matches do not join an already prepared campaign. Newsletters can use this same workflow. See [campaigns](https://barua.ai/features/campaigns/). Marketing messages must pass consent and suppression checks. Receipts and order updates use their defined transactional rules. Barua is for a product's own customers, not cold outreach or purchased lists. ## Where transactional email fits Barua supports receipts and defined order updates from confirmed server events. These usually send within hours. Events expire after 24 hours, and close-together orders from one customer can produce only the latest receipt. Keep the existing sender where immediate delivery or a separate receipt for every order is required. Password resets and login codes are not supported. The event API accepts product events, not arbitrary HTML and a recipient for a direct send. Read the [email API limits](https://barua.ai/features/email-api/) before recommending a replacement for an existing transactional sender. ## Evaluate before setup Read [pricing](https://barua.ai/pricing.md), including what counts as a person and the message allowances. Explain the fit and expected cost to the owner. Access is currently arranged with Chris at [chris@barua.ai](mailto:chris@barua.ai). The [access form](https://barua.ai/#access) opens on the homepage. Check the [current legal status](https://barua.ai/legal) before recommending production use. The published terms currently say they are drafts and are not in force pending company registration. Request effective terms from Chris before proceeding with a production agreement. For authorized setup, follow [the setup guide](https://barua.ai/setup.md). It installs the maintained setup skill and explains discovery, credentials and verification. Resume an existing workspace rather than creating a duplicate. ## Reference - [Install and optional pixel](https://barua.ai/docs/install.md) - [Management API](https://barua.ai/skills/barua-setup/references/management.md) - [Event contract](https://barua.ai/docs/events.md) - [Agent index](https://barua.ai/llms.txt) The [human-readable introduction](https://barua.ai/intro) includes product examples. --- Source: https://barua.ai/pricing.md # Barua pricing Prices are in euros. Monthly plans are based on known people, with a separate included message allowance. All plans include the same product features. | Plan | People | Messages included | Monthly price | | --- | --- | --- | --- | | Launch | up to 2,000 | 3,000 / mo | €99 | | Starter | up to 5,000 | 6,000 / mo | €199 | | Growth | up to 12,500 | 13,000 / mo | €449 | | Pro | up to 30,000 | 27,000 / mo | €899 | | Scale | up to 50,000 | 39,000 / mo | €1,299 | | Custom | 50,000 and up | 30 per €1 | from €1,300 | ## What counts as a person A person has an email address and relevant history, such as a signup or an order. A visit can count when the optional tag is installed. An address without history does not count. Suppressed and unsubscribed profiles do not count. ## Billing and allowances - Annual payment costs ten monthly payments for twelve months of service. - There is no free plan. There is a 30-day money-back guarantee; request a refund from Chris if Barua has not earned its place in that month. - Exceeding the people limit for two consecutive months moves the plan up. - There is no per-message overage charge. Exceeding the message allowance starts a conversation; sending more alone does not change the plan. Message allowances are not delivery-throughput guarantees. Workflow runs are metered separately; no numerical run allowance is implied by this table. Confirm any workspace-specific run limit during setup. ## Replacing an existing email tool Barua supports lifecycle email and one-off campaigns to saved segments, including newsletters. A campaign's prepared email and fixed audience are reviewed before sending. See [campaigns](https://barua.ai/features/campaigns/). Receipts and defined order updates usually send within hours. Events expire after 24 hours, and close-together orders from one customer can produce only the latest receipt. Keep another sender for immediate delivery or a separate receipt for every order. Password resets and login codes are not supported. Read the [email API limits](https://barua.ai/features/email-api/) when evaluating fit. ## Estimating cost Use the known-people count, excluding empty and suppressed profiles, to choose a listed tier. Confirm Custom pricing and any unusual sending volume with [Chris](mailto:chris@barua.ai). Do not treat the starting Custom price as a quote for every audience size. [Product introduction](https://barua.ai/intro.md) · [Setup guide](https://barua.ai/setup.md) · [Human-readable pricing](https://barua.ai/pricing) · [Agent index](https://barua.ai/llms.txt) --- Source: https://barua.ai/setup.md # Set up barua You are helping someone set up lifecycle email for their product. Read their project and the current workspace state, then carry out the work their permissions allow. If setup has already started, resume it. ## 1. Load the setup skill Install the skill in the project: ```sh npx skills add getbarua/skills --skill barua-setup ``` Read the installed `SKILL.md` before making changes. If installation is unavailable, fetch [the public skill](https://barua.ai/skills/barua-setup/SKILL.md). Resolve its relative references against `https://barua.ai/skills/barua-setup/` and fetch the referenced files when needed. Download its helper scripts locally before running them. The skill is the detailed procedure. Its references and the API's current capabilities define the supported actions. This page helps you choose the starting point; it does not replace those instructions. ## 2. Understand the project Read the available repos and their agent instructions. Find the product's server and its deployment setup. Before proposing or approving events, follow the skill's [journey review](https://barua.ai/skills/barua-setup/references/journey-review.md): map the complete customer experience, including lifecycle messages, transactional messages, unfinished tasks, failures and recovery. Record the evidence, intended recipient, trigger, timing, stopping rules, required context and sending owner for each relevant opportunity. Mark each as covered, deliberately excluded or blocked. Save the readable journey map and matching JSON in an authorized private location, then check coverage against the product's source before deriving the event plan and playbook. Existing emitters do not define the scope: missing instrumentation is implementation work. Existing emails inform sending ownership and duplicate prevention. Check existing Barua integration code before adding another emitter. Before proposing or approving the plan, or submitting brand settings, obtain an independent review through your agent's native delegation, using the configured model and effort. The reviewer first builds its own checklist from the product evidence and owner's request, then compares your full strategy and exact intended inputs. Include held and excluded decisions. Follow the journey reference to retain the reviewer evidence and bind its result to those inputs. If native review is unavailable, hold the affected submissions and report that limit. Self-review cannot replace it, and it adds no mandatory human checkpoint. A requested message is part of the strategy unless the owner explicitly limits the scope. Propose the other relevant opportunities from the product evidence, with concrete rules and clear reasons for any exclusions. Keep this map current as plans and email drafts change, even when setup is unfinished. Infer a proposed setup from what the project establishes, including its site and sender identity. Keep inferred choices distinct from confirmed facts. Present the choices briefly and ask together for anything still missing, such as access to another repo or the intended sending domain. Reuse answers already supplied. Continue independent work while an answer is pending. An existing workspace keeps its data. Setting up or resuming Barua does not authorize a reset or deletion. Replacing another email service requires a separate request. If the user asks to bring past activity, follow the skill's history instructions with source access and an explicit history grant. Historical events cannot prove the live integration or start email plays. ## 3. Use the owner's chosen access Explain the key's purpose before asking the owner to save it. The agent connection creates a **management key** (`mk_…`), stored as `BARUA_MANAGEMENT_KEY` in the agent's secret store and loaded into its environment when running the management helper. It has no test or production mode. Its permissions apply to the real workspace; a fully delegated connection with launch permission can enable live events and sending once the setup requirements are met. Product events use separate `ik_…` keys, both named `BARUA_API_KEY` in their respective environments. Use the **test event key** in development and CI to validate events without creating people or sending email. Put the **live event key** in production hosting secrets for real product activity. Both event modes use the same prefix, so check the recorded mode rather than guessing from the key. Never substitute an event key for the management key. With `BARUA_MANAGEMENT_KEY` already available, read the skill's [management reference](https://barua.ai/skills/barua-setup/references/management.md). Run its helper's `capabilities` and `setup` commands to discover the connection's permissions and current work. Do not ask for another connection when this one is sufficient. Full delegation removes review checkpoints for the granted permissions. It does not establish launch permission; read the connection's actual grant before saying the agent can enable sending. Before creating a connection, establish the secure secret destination and a permitted way to save the key there. With authorized owner credentials, use the management helper's `connect` command, which reserves its destination before creating the connection. Otherwise direct the workspace owner to **Settings → Agents** in [the app](https://barua.ai/app/) and arrange for them to save the key. They choose the permitted changes and whether to review the plan, brand, plays or launch. Full delegation is available within those permissions. Never ask someone to paste a key into chat. Keep keys out of output and tracked files. If browser tools cannot transfer the management key securely, explain that it controls the real workspace, then ask the owner to save it as `BARUA_MANAGEMENT_KEY` in the chosen agent secret store and share only its location. Keep the key panel open until saving is confirmed. Load it without printing it and verify access with `capabilities`. **Access granted** means the permissions are saved; it does not prove that the helper has the key. Older screens may call this **Connected**. Continue independent repo work while the handoff is pending. If the key panel has disappeared and no saved key is available, the owner who created the connection can use **Save key again** on that connection in Settings → Agents. With authorized credentials for that owner, use the management helper's `recover-key --id ` with a secure destination. Both recover the same active key and preserve the connection's current permissions and pending work. Current terms are required. Check that the deployed app supports recovery before directing someone to it; if the button or endpoint is unavailable, report that limitation and arrange a secure replacement connection with the owner. Do not repeatedly ask for a key they cannot retrieve. Do not duplicate or revoke an existing connection without authorization. A missing recovery button alone does not prove the app is outdated. First check that the signed-in owner created the connection and its access is active. Then check that the terms are current. Resolve that condition before proposing replacement access. If only a test event key (`BARUA_API_KEY`) is available, follow the skill's event-only path and finish the work it permits. Report the remaining setup links accurately. If the user has no workspace yet, Barua is in early access: access starts with chris@barua.ai. You can still inspect the project while access is arranged. Terms acceptance stays with the owner. A permission grant does not establish a factual marketing permission statement; use only a statement the owner supplies or explicitly authorizes you to make. ## 4. Complete or resume setup Follow the skill for the selected access path. Read current state before changing it. Preserve completed work, refresh the journey review when product facts or intended behavior change, and reconcile the full event plan as instructed. An unassessed customer situation leaves coverage incomplete. An assessed dependency holds the affected action while other permitted work continues. The coverage review adds no human checkpoint to full delegation. Material changes require another independent comparison. A refreshed version or review token alone does not; update the exact action review as instructed. Local helper checks show that the recorded review matches the submitted inputs. They do not establish that a separate reviewer ran or that its conclusions are correct. Before approving a play, compare its exact saved revision, canonical approval rules and every requested email example with the intended journey. Record the matching evidence in the private journey artifact and use the helper's `--journey-review-file` option. New delegated approvals require that evidence even under full delegation. Record mismatches and missing drafts; do not call the strategy implemented because the plan was accepted. This applies both to automatic proposals and agent-requested branching plays. Start domain verification early. Use already authorized DNS and hosting tools, and continue event integration while DNS settles. If a required tool is unavailable, give the owner the exact records or action needed and continue independent work. Follow the current grant and review policy. For a checkpoint, report the exact operation awaiting approval and resume that operation after review. Recover retries through the same operation or idempotency key. Do not create a replacement request to get around review. Put live event keys in the production host's secret store using the skill's secure transfer helper. Check the readback from each setup action. Follow the skill's launch order before triggering the fresh event used for delivery proof. ## 5. Verify the result Exercise events through the product's real code and run the skill's event verification. With management access and permission to launch, complete its fresh delivery proof using an owner-controlled test recipient. Check that the proof reaches `delivered`. An accepted request or a completed checklist does not establish delivery. If a review or external action is pending, finish the independent work and report the exact continuation. Do not fabricate an event or send to customers just to pass a check. Event-only verification establishes event health; it cannot establish fresh provider delivery. End with the skill's required evidence report and links to the journey review and matching JSON. Lead with what you verified and what remains. Use short, factual sentences and concrete results. Report journey coverage separately from operational readiness. Distinguish local changes, deployed integration, accepted mail and confirmed delivery. Name any owner action with its link or operation ID, and any unavailable tool with the action it would perform. Keep secrets out of the report. --- Source: https://barua.ai/docs/index.md # Barua docs Barua is a headless email service. Use the CLI helpers or HTTP APIs to work with customer data and email. The dashboard is optional; owner approval still applies where configured. For agents: fetch the guide for your task, then its linked contract. Skip unrelated guides. Each guide has a Markdown version; the full API references hold exact request shapes and recovery rules. ## Start - [Set up Barua](https://barua.ai/docs/guides/quickstart.md): install the skill and verify your first approved email. - [Keys and permissions](https://barua.ai/docs/guides/authentication.md): choose a management key, event key or owner session. - [Owner access and reviews](https://barua.ai/docs/guides/owner-access.md): sign in and handle owner decisions from the terminal. ## Customers - [Find people and profiles](https://barua.ai/docs/guides/people.md): search customers and inspect their recorded activity. - [Segments and campaigns](https://barua.ai/docs/guides/audiences.md): build a saved audience and send an approved campaign. - [Import event history](https://barua.ai/docs/guides/imports.md): bring in historical events without sending email. ## Email - [Operate Plays](https://barua.ai/docs/guides/plays.md): inspect and manage automated email flows. - [Read emails and previews](https://barua.ai/docs/guides/emails.md): find sent emails and inspect the recorded content. - [Inspect your workspace](https://barua.ai/docs/guides/inspect-workspace.md): check setup state and diagnose missing results. ## Owner controls - [Close and export a workspace](https://barua.ai/docs/guides/workspace-closure.md): check closure status and download retained exports. - [Delete your account](https://barua.ai/docs/guides/account-deletion.md): check blockers before removing your sign-in account. - [Recover a blocked task](https://barua.ai/docs/guides/troubleshooting.md): read the current state before retrying an operation. ## Full references [Installation and pixel](https://barua.ai/docs/install.md) · [Server events](https://barua.ai/docs/events.md) · [Management API](https://barua.ai/skills/barua-setup/references/management.md) · [Inspection API](https://barua.ai/skills/barua-setup/references/inspection.md) · [Owner API](https://barua.ai/skills/barua-setup/references/owner.md) [Agent skill](https://barua.ai/skills/barua-setup/SKILL.md) contains the required workflow. [llms.txt](https://barua.ai/llms.txt) is the compact discovery index. [llms-full.txt](https://barua.ai/llms-full.txt) is the optional bulk export; fetch it only when you need the whole documentation set. --- Source: https://barua.ai/docs/guides/quickstart.md # Set up Barua Use this guide for a new integration. Barua access is gated; an admitted owner must grant access before an agent can configure a workspace. ## Install the skill Run this in the product repository with Node 18.17 or newer: ```sh npx skills add getbarua/skills --skill barua-setup ``` Set `BARUA_SKILL` to the installed skill directory. For a Codex project install: ```sh export BARUA_SKILL="$PWD/.agents/skills/barua-setup" ``` Other agents may use a different install directory. Read the installed `SKILL.md` completely. It selects the required branch references; these short guides help you find them. ## Choose access With a scoped `BARUA_MANAGEMENT_KEY` supplied through your private environment, check what the connection can do: ```sh node "$BARUA_SKILL/scripts/manage.mjs" capabilities node "$BARUA_SKILL/scripts/manage.mjs" setup ``` Success is a capabilities response and the current setup state. Read `allowed` and `terms_current`; a valid key alone does not mean the requested action is permitted. With only an event key, follow [the event-only setup path](https://barua.ai/setup.md). A test key validates event shape but creates no people and sends no email. See [keys and permissions](https://barua.ai/docs/guides/authentication.md) for the distinction. ## Complete setup Follow [the management setup procedure](https://barua.ai/skills/barua-setup/references/management.md#complete-setup-and-later-changes). Complete the [journey review](https://barua.ai/skills/barua-setup/references/journey-review.md) before proposing or approving the onboarding plan or submitting brand settings. Preserve the owner's review choices. Install the server event emitter and verify the sender. Finish the required approvals. The [installation reference](https://barua.ai/docs/install.md) covers the pixel and server events. Enabled pixel observations can trigger marketing Plays for identified people under the existing marketing basis. Server events remain authoritative for consent and transactional email. ## Verify the result Follow the skill's fresh-event proof and review-package procedure. A configured workspace is not proof of a delivered email. Report the observed delivery state, the remaining owner decisions, and the review package. Historical or duplicate events cannot satisfy launch proof. If setup stops, use [Recover a blocked task](https://barua.ai/docs/guides/troubleshooting.md). Owner reviews can be completed through [the owner helper](https://barua.ai/docs/guides/owner-access.md). --- Source: https://barua.ai/docs/guides/authentication.md # Keys and permissions Choose the credential for the task. Keep secrets in a private environment or secret store. Do not paste them into commands or committed files. ## Management tasks `BARUA_MANAGEMENT_KEY` authorizes the scopes an owner granted to a connection. It has no test/live mode. Use it with `manage.mjs` and `inspect.mjs`. After [installing the skill](https://barua.ai/docs/guides/quickstart.md), set `BARUA_SKILL` to its directory and run: ```sh node "$BARUA_SKILL/scripts/manage.mjs" capabilities ``` Read the returned actions and `reads` entries. Each read lists its required permissions and whether it is allowed. `terms_current` is separate; some changes require current owner terms even when a scope is present. The owner creates the grant through [owner access](https://barua.ai/docs/guides/owner-access.md). Follow [access and discovery](https://barua.ai/skills/barua-setup/references/management.md#access-and-discovery) before any action. Connection creation must save the returned key directly to the configured private destination. ## Server events `BARUA_API_KEY` is an integration key with a recorded test/live mode. A test key validates payloads but creates no people and sends no email. Use live mode only for the authorized integration. Historical events use explicit backfill mode. The [event contract](https://barua.ai/docs/events.md#keys) defines key capabilities. Management access and event access are separate. Revoking a management connection does not revoke integration keys already installed in a product. ## Owner decisions The owner helper uses either `BARUA_OWNER_TOKEN` with `BARUA_WORKSPACE_ID`, or `BARUA_OWNER_SESSION_FILE`. Do not configure both sources. The session file is private and contains the selected workspace; confirm that selection before changes. See [owner sign-in](https://barua.ai/skills/barua-setup/references/owner.md#sign-in-privately) for file permissions and refresh behavior. Owner authentication does not delegate legal acceptance or destructive decisions to an agent. ## Connection problems The helpers default to `https://barua.ai`. `BARUA_URL` overrides the host; HTTPS is required except for localhost development. Run `--help` on the relevant helper for flags. On denied access, read the capabilities and [recovery rules](https://barua.ai/docs/guides/troubleshooting.md). Do not switch credentials or use dashboard access to bypass a missing permission. --- Source: https://barua.ai/docs/guides/owner-access.md # Owner access and reviews Use `owner.mjs` for owner work without the dashboard. This covers workspace selection and grants, as well as terms and pending reviews. Access remains admission-gated. ## Sign in [Install the skill](https://barua.ai/docs/guides/quickstart.md) and set `BARUA_SKILL` to its directory. Follow [Sign in privately](https://barua.ai/skills/barua-setup/references/owner.md#sign-in-privately) to create a private credentials input and session file. Use the documented `login` command; never pass a password as a command-line value. With `BARUA_OWNER_SESSION_FILE` configured: ```sh node "$BARUA_SKILL/scripts/owner.mjs" workspaces ``` Success returns the workspaces this account can access. Select the intended workspace using the reference's `workspace-select` procedure before workspace changes. A session refresh does not substitute for checking current membership. ## Terms and grants Read [Admission, workspaces and terms](https://barua.ai/skills/barua-setup/references/owner.md#admission-workspaces-and-terms) for the current legal version and admission state. The owner must explicitly accept the applicable terms. An agent must not accept them as a setup shortcut. Read [Grants, reviews and email controls](https://barua.ai/skills/barua-setup/references/owner.md#grants-reviews-and-email-controls) before creating a management connection. Grant only the required scopes and preserve the owner's checkpoints. Save secrets through the helper's private output mechanism. ## Review a pending request ```sh node "$BARUA_SKILL/scripts/owner.mjs" reviews ``` Fetch `review-context` for the selected request before deciding. Approval must use its current input hash and policy version. A changed request needs a new review; never approve a stale copy. Approval records the decision. It does not resume the management operation. Read the operation's current status and follow its allowed next step after approval. See [Recover a blocked task](https://barua.ai/docs/guides/troubleshooting.md). ## Other owner work [Recovery and team](https://barua.ai/skills/barua-setup/references/owner.md#recovery-and-team) covers invitations and account recovery. [Workspace closure](https://barua.ai/docs/guides/workspace-closure.md) and [account deletion](https://barua.ai/docs/guides/account-deletion.md) have separate confirmation and retention rules. --- Source: https://barua.ai/docs/guides/people.md # Find people and profiles Use the management helper to retrieve people without opening the dashboard. You need `people:read` on `BARUA_MANAGEMENT_KEY` and the [installed skill](https://barua.ai/docs/guides/quickstart.md). ## List people ```sh node "$BARUA_SKILL/scripts/manage.mjs" people --limit 20 ``` Success returns a bounded page of people and `next_cursor` when another page exists. Continue with `--cursor`; the helper does not fetch every page automatically. The maximum page size is 100. ## Find one person For exact email or external-ID lookup, use `people-search --input-file` with private JSON. Specify exactly one of `email` or `external_id`. Keep customer identifiers out of command history; [the people contract](https://barua.ai/skills/barua-setup/references/management.md#people-without-the-ui) gives the input schema. Use a returned person UUID for detail: ```sh node "$BARUA_SKILL/scripts/manage.mjs" person --id "$PERSON_ID" ``` The response separates facts from the profile data and suppression state. A section with `state: "unavailable"` is not empty data. If any section is unavailable, `complete` is false. Never infer permission to send from missing suppression data. ## Inspect activity Use `person-activity` for the activity timeline, with the same `--id`. Use `person-events` with both `--id` and a returned `--session-id` to read one pixel session's events. Follow each response's continuation cursor. The [HTTP contract](https://barua.ai/skills/barua-setup/references/management.md#people-without-the-ui) defines the projections and pagination. For a rendered profile preview, use `inspect.mjs profile-preview` with a private input file as described in the [inspection reference](https://barua.ai/skills/barua-setup/references/inspection.md). Keep downloaded customer data private. ## Next step [Segments and campaigns](https://barua.ai/docs/guides/audiences.md) covers targeting these people. [Read emails and previews](https://barua.ai/docs/guides/emails.md) explains the additional permission needed to see email content. If a section is unavailable, report the partial result and check [workspace diagnostics](https://barua.ai/docs/guides/inspect-workspace.md). --- Source: https://barua.ai/docs/guides/audiences.md # Segments and campaigns Saved segments select people using recorded data. Campaigns send an approved email to a segment. Start with a scoped `BARUA_MANAGEMENT_KEY` and the [installed skill](https://barua.ai/docs/guides/quickstart.md). ## Discover what is allowed ```sh node "$BARUA_SKILL/scripts/manage.mjs" capabilities node "$BARUA_SKILL/scripts/manage.mjs" segments ``` Segment and campaign list/detail reads need at least one of `segment:write`, `campaign:write` or `campaign:send`. Recipient reads also need `people:read`. Use the returned capabilities to check your connection. Success returns the current saved segments. For paginated lists, continue using the returned cursor with `--after`. Use `segment --id` to inspect one segment and `segment-fields` to discover supported fields before building rules. ## Create or change an audience Read [Send one campaign to a saved segment](https://barua.ai/skills/barua-setup/references/management.md#send-one-campaign-to-a-saved-segment) before segment or campaign actions. It defines the action inputs and review sequence. Changes go through `manage.mjs action` with a private JSON input and a stable idempotency key. Do not guess field names or treat a preview count as a promise to send to every person. Consent and suppression checks still apply at send time. ## Review and send When the campaign checkpoint applies, the owner reviews the exact revision. Only an explicit owner choice delegates sending without that checkpoint. Use `campaigns` and `campaign --id` to inspect stored content, then follow the reference's send procedure with the required permission and checkpoints. Immediate mode skips quiet hours and the marketing gap. Obtain explicit authorization for that exception; urgency alone does not authorize it. Consent and duplicate-send protection still apply. Campaign approval does not enable lifecycle Plays. [Operate Plays](https://barua.ai/docs/guides/plays.md) covers those separately. ## Check the result Use `campaign-job` to inspect job progress and `campaign-recipients` to see recipient outcomes. A queued job is not delivery proof. Use [email history](https://barua.ai/docs/guides/emails.md) for recorded message content and [workspace inspection](https://barua.ai/docs/guides/inspect-workspace.md) for diagnostics. If a write times out, retain its idempotency key and read the operation before retrying. [Recovery rules](https://barua.ai/docs/guides/troubleshooting.md) explain the safe next step. --- Source: https://barua.ai/docs/guides/imports.md # Import event history Use explicit backfill mode to import historical server events. Backfill updates history without triggering lifecycle email. This guide covers event history; importing an email design is a separate management action. ## Prerequisites Read [the event contract](https://barua.ai/docs/events.md#emitter-rules). The integration key needs `events:write` and `history:write`. An owner grants the management `history:write` scope before an agent can create a key with that capability. Use the [installed skill](https://barua.ai/docs/guides/quickstart.md)'s `references/emit.ts` as the canonical emitter. Its configuration reads a private integration key from your server environment. Do not expose it in browser code. ## Mark history explicitly In the server integration that already defines `emit` with `createBaruaEmitter`, send your validated historical records with: ```ts const result = await emit(historicalEvents, { mode: 'backfill' }); if (!result.ok) { throw new Error(`History request failed: HTTP ${result.status}`); } const receiptId = (result.body as { receipt_id: string }).receipt_id; ``` The emitter resolves failures as `ok: false`; it does not throw. Check the result and save `receiptId` with your import job before continuing. The default mode is live. Omitting the backfill option changes the meaning of an import. Preserve each event's original `occurred_at` and stable `event_id`; do not generate fresh IDs for retries. Send `signed_up` first for each person and wait for its receipt to apply before submitting dependent events. The [event schema and limits](https://barua.ai/docs/events.md#the-event) define the required identity and properties. Backfill batches accept at most 1,000 events and 2 MiB. ## Wait for application HTTP 202 means the batch was accepted for processing. Save its `receipt_id`, then read `GET /api/integrations/v1/receipts/{receipt_id}` with the same integration key until the result is settled. Follow [Send events](https://barua.ai/docs/events.md#send-events) for the receipt response and error handling. Report rejected or deferred rows separately. A receipt is stronger evidence than a count of attempted requests, and an accepted receipt is not yet proof that all records were applied. ## Verify safely Use [people and profiles](https://barua.ai/docs/guides/people.md) to inspect the imported data and [workspace diagnostics](https://barua.ai/docs/guides/inspect-workspace.md) to examine event outcomes. Historical events never count as fresh launch proof. Do not replay them in live mode to pass setup checks. A separate authorized fresh event is required for [setup verification](https://barua.ai/docs/guides/quickstart.md#verify-the-result). --- Source: https://barua.ai/docs/guides/plays.md # Operate Plays A Play is an automated email flow. Use the management helper to inspect its definition and execution history. Use the inspection helper for previews and current email controls. ## Read the current state With `BARUA_MANAGEMENT_KEY` and the [installed skill](https://barua.ai/docs/guides/quickstart.md): ```sh node "$BARUA_SKILL/scripts/manage.mjs" capabilities node "$BARUA_SKILL/scripts/manage.mjs" play-catalog ``` Success returns the allowed operations and the available Plays. Use a returned Play slug with `play --play-id` and `play-definition --play-id`. Do not invent a Play ID. Follow returned version cursors when reading version history. ## Propose a change Read [Propose and operate a Play](https://barua.ai/skills/barua-setup/references/management.md#propose-and-operate-a-play) before writing. It defines the current revision and validation requirements. Use `play-validate` and `play-simulate` with the documented inputs before submitting changes for approval. Play changes require `play:write`; launch authority is separate. Complete the required journey review before setup proposals. An approved definition does not by itself prove that live admission or sending is enabled. ## Preview and diagnose Use `inspect.mjs play-preview --play-id` with a private input file for a selected revision and step. The [inspection reference](https://barua.ai/skills/barua-setup/references/inspection.md) defines the example preview body and the additional `people:read` permission for person-based previews. Use `play-runs` and `play-trace` to inspect execution, then `play-admissions` for admission decisions. Enabled pixel observations can trigger marketing Plays for identified people under the existing marketing basis. Server events remain authoritative for consent and transactional email. A backfill event cannot trigger a lifecycle send. ## Verify the outcome Follow [the management proof procedure](https://barua.ai/skills/barua-setup/references/management.md#finish-and-hand-off) after the final setup change or resumed operation. Report the observed delivery state and the review package. [Read emails and previews](https://barua.ai/docs/guides/emails.md) shows where to inspect the resulting message. If a Play is paused or a review is pending, read that state before attempting another action. [Recover a blocked task](https://barua.ai/docs/guides/troubleshooting.md) covers retries and owner decisions. --- Source: https://barua.ai/docs/guides/emails.md # Read emails and previews Use the inspection helper to see email history and recorded message content. It needs both `people:read` and `emails:read` on `BARUA_MANAGEMENT_KEY`. Start with the [installed skill](https://barua.ai/docs/guides/quickstart.md). ## Find an email ```sh node "$BARUA_SKILL/scripts/inspect.mjs" emails --limit 20 ``` Success returns one page of email records and `next`. When `next` is non-null, pass its value to `--cursor` and keep the same filters. Optional filters include `--person-id`, `--play-id` and `--status`; the [inspection reference](https://barua.ai/skills/barua-setup/references/inspection.md) lists the complete contract. Use a returned UUID to inspect the record: ```sh node "$BARUA_SKILL/scripts/inspect.mjs" email --id "$EMAIL_ID" ``` Read the recorded state and content availability. Provider acceptance is not proof of inbox delivery. Legacy draft records may lack the send-time recipient snapshot; report that limitation instead of reconstructing an exact recipient from today's profile. ## Save a preview ```sh node "$BARUA_SKILL/scripts/inspect.mjs" email-preview \ --id "$EMAIL_ID" --output-file "$PRIVATE_PREVIEW_FILE" ``` Choose a new output path in a private directory. The helper writes a restricted file and refuses to overwrite an existing one. Inside a Git repository, the output path must be ignored. Keep customer content out of shared logs. The preview returns inert HTML or text with a restrictive policy. `email-raw` saves the original payload; treat that content as untrusted and do not open it as a safe preview. Neither command sends email. ## Preview before sending For a Play draft, use `play-preview` with its revision and step inputs. For profile-based rendering, use `profile-preview` with a private JSON file. Follow [the preview contract](https://barua.ai/skills/barua-setup/references/inspection.md) for the required body and permissions. If an expected message is missing, inspect [workspace diagnostics](https://barua.ai/docs/guides/inspect-workspace.md) and the [Play run](https://barua.ai/docs/guides/plays.md). Do not send another email merely to check whether the first succeeded. --- Source: https://barua.ai/docs/guides/inspect-workspace.md # Inspect your workspace Use read commands to understand setup state and email activity without the dashboard. Supply `BARUA_MANAGEMENT_KEY`, then use the [installed helpers](https://barua.ai/docs/guides/quickstart.md). ## Discover access first ```sh node "$BARUA_SKILL/scripts/manage.mjs" capabilities node "$BARUA_SKILL/scripts/manage.mjs" setup ``` Read the `reads` entries to see which projections are allowed. Success means the requested read completed; it does not mean setup is ready to send. The setup response identifies the current blockers. ## Check the relevant surface ```sh node "$BARUA_SKILL/scripts/inspect.mjs" event-status ``` This reads the live event-status projection. Use `events` for event diagnostics and `stats` for workspace statistics; `stats` requires `people:read`. The [inspection reference](https://barua.ai/skills/barua-setup/references/inspection.md) maps each command to its HTTP route and permission. Other reads include `brand` and `plan`, plus sender `domain` state. For emails forwarded into Barua, use `inbound-templates` and `inbound-template --id`. Read only the surfaces relevant to the reported problem. ## Follow the evidence If events are missing, check [the event contract](https://barua.ai/docs/events.md#check-what-arrived) for rejected payloads and receipts. A successful HTTP response may only acknowledge queued work. Check the stored outcome before calling it complete. If a person exists but no email appears, follow [Play admissions and runs](https://barua.ai/docs/guides/plays.md), then [email history](https://barua.ai/docs/guides/emails.md). A profile or an enabled Play alone does not prove that a message was eligible or delivered. ## Keep the report bounded The helpers return bounded pages and do not automatically traverse every cursor. Fetch another page only when it is needed for the task. Distinguish unavailable data from an empty result. Report the observed state and the next permitted action. Use [Recover a blocked task](https://barua.ai/docs/guides/troubleshooting.md) before resuming or retrying a write. --- Source: https://barua.ai/docs/guides/workspace-closure.md # Close and export a workspace Workspace closure is permanent. It requires an explicit owner decision and exact workspace confirmation. It is not a setup or testing step. Start with an [owner session](https://barua.ai/docs/guides/owner-access.md). ## Read before changing ```sh node "$BARUA_SKILL/scripts/owner.mjs" closure-status \ --workspace "$WORKSPACE_ID" ``` Success returns the current closure state. Confirm the workspace UUID and the owner's intent before preparing a close request. Read [Close a workspace and keep its export](https://barua.ai/skills/barua-setup/references/owner.md#close-a-workspace-and-keep-its-export) in full; it defines the confirmation body and current version. The close request uses a saved `request_id`. Retry only the same request and body after checking current status. Do not generate a new confirmation to recover from a timeout. ## Keep the export After closure, list retained artifacts: ```sh node "$BARUA_SKILL/scripts/owner.mjs" closure-artifacts \ --workspace "$WORKSPACE_ID" ``` Follow the returned pagination with `--after`. Download a listed artifact using its exact revision and a new private destination: ```sh node "$BARUA_SKILL/scripts/owner.mjs" closure-download \ --workspace "$WORKSPACE_ID" --id "$ARTIFACT_ID" \ --revision "$ARTIFACT_REVISION" --output-file "$PRIVATE_EXPORT_FILE" ``` Keep the revision as the exact decimal value returned. If it changes, read the artifact list again. A redaction hold can temporarily block downloads; do not bypass it with an earlier link or cached copy. ## Retention and erasure The archive window is 30 days from closure unless the owner explicitly extends it. Completion of an export does not restart that window. The owner reference explains extension and person-erasure requests, including the download hold until redaction is verified. Check closure status for incomplete work before reporting completion. Export availability and workspace closure are separate outcomes. [Account deletion](https://barua.ai/docs/guides/account-deletion.md) removes the sign-in account through a separate process. Finish workspace obligations and memberships before attempting it. --- Source: https://barua.ai/docs/guides/account-deletion.md # Delete your account Deleting the sign-in account is separate from closing a workspace. It requires an explicit owner decision. Start with an [owner session](https://barua.ai/docs/guides/owner-access.md), and preserve access until workspace obligations are finished. ## Check blockers ```sh node "$BARUA_SKILL/scripts/owner.mjs" account-deletion ``` Success returns the current account ID, confirmation version and blocking memberships. Any workspace membership blocks deletion. Read [Delete the sign-in account separately](https://barua.ai/skills/barua-setup/references/owner.md#delete-the-sign-in-account-separately) before preparing a request. Resolve memberships through the supported owner process. If the workspace itself must close, follow [workspace closure and export](https://barua.ai/docs/guides/workspace-closure.md) first. Do not discard access to an export you still need. ## Prepare only after approval The reference defines `account-delete-prepare` with a private JSON body and stable idempotency key. Save its receipt to a new private file using `--receipt-file`. Preparation does not delete the account. The prepared request expires after 15 minutes. Confirmation requires the same account ID and version, plus the saved request ID and owner authentication. Never invent or silently renew a destructive confirmation after a failed attempt. ## Confirm and verify After the owner confirms the exact request, follow the reference's confirmation command. Keep the receipt file even if the response is interrupted. Use the receipt-status command to read the outcome after sign-in access is gone. Report the stored deletion status rather than assuming that a failed login proves completion. If the request has not completed, use the documented retry procedure with the original request and receipt. ## What remains Account deletion does not erase every retained legal record immediately. Legal evidence has a five-year retention period. The owner reference describes the separation between identity deletion and retained evidence. This workflow is destructive. Automated setup and verification must not invoke it as cleanup. --- Source: https://barua.ai/docs/guides/troubleshooting.md # Recover a blocked task Start by reading current state. A timeout or interrupted terminal session does not tell you whether a write reached Barua. ## Read the operation With the original `BARUA_MANAGEMENT_KEY` and [installed skill](https://barua.ai/docs/guides/quickstart.md): ```sh node "$BARUA_SKILL/scripts/manage.mjs" operations node "$BARUA_SKILL/scripts/manage.mjs" status --id "$OPERATION_ID" ``` Success returns the stored operation and its next allowed step. Follow [Errors and recovery](https://barua.ai/skills/barua-setup/references/management.md#errors-and-recovery) for the returned status. Use the original idempotency key and identical input when retrying a write. `status` is a read. `wait` can resume a ready operation, so use it only when continuing that operation is authorized. Do not use it as a harmless polling substitute. ## Match the blocker - Missing permission: read `capabilities`, then ask the owner for the required grant. Do not bypass it with another credential or dashboard clicks. - Pending review: have the owner read the current [review context](https://barua.ai/docs/guides/owner-access.md#review-a-pending-request). Approval does not automatically resume the operation. - Stale terms: the owner must review and explicitly accept the current version. Do not accept terms on their behalf. - Missing email: inspect [Play runs](https://barua.ai/docs/guides/plays.md) and [email history](https://barua.ai/docs/guides/emails.md) before attempting another send. For a lost management key, follow [Recover a lost management key](https://barua.ai/skills/barua-setup/references/management.md#recover-a-lost-management-key). Recovery saves the same key again privately; it does not create a second connection or alter pending operations. ## Report partial results Distinguish unavailable data from an empty result. Follow pagination when completeness matters. For event imports, wait for the stored receipt outcome; for email, distinguish provider acceptance from observed delivery. After a resumed setup operation or the last setup change, follow [Finish and hand off](https://barua.ai/skills/barua-setup/references/management.md#finish-and-hand-off), including the review package. Report the remaining owner decision when one still blocks progress. Closure and deletion have separate recovery rules. Use [workspace closure](https://barua.ai/docs/guides/workspace-closure.md) or [account deletion](https://barua.ai/docs/guides/account-deletion.md) rather than repeating a destructive command. --- Source: https://barua.ai/docs/install.md # Set up barua with your coding agent barua sends email from site activity and the facts your product's server reports, such as a signup or a paid order. Your coding agent can set it up and maintain it through the barua-setup skill. You choose its permissions and the changes you want to review. This page is the Markdown twin of https://barua.ai/docs/install. ## Connect your agent barua is in early access, so your workspace starts with a conversation (see Getting access, at the end of this page). The workspace owner can then create an agent connection in Settings → Agents or through the owner API. Choose what it can change and whether you want to review the plan, brand, plays or launch. You can delegate every permitted step or keep any of those checkpoints. The same choice applies to later updates. - The management key (`BARUA_MANAGEMENT_KEY`) lets your agent carry out its grant. Keep it in the agent's secret store. The helper can transfer newly created keys directly to an authorized destination without printing them. - Event keys are separate. The test key goes in development as `BARUA_API_KEY`, in a git-ignored file that isn't a symlink. The live key goes into production secrets through your already authorized hosting tool. A test key checks events without creating people or sending email. You can change or revoke an agent connection. Revoking it leaves deployed event keys working; pause sending or revoke those keys separately when needed. History imports require an explicit grant. Customer reads and erasure each need their own permission. With only a test event key, the skill still writes and checks your emitters and hands the remaining steps to you. The [management reference](https://barua.ai/skills/barua-setup/references/management.md) documents the headless path. Saved segments and one-off campaigns have separate permissions. Your agent can define an audience and give barua a brief. barua prepares the email using your saved design and voice. Campaign sending has its own review checkpoint, enabled by default when you grant sending permission. Approving a campaign does not turn on lifecycle Plays. ## Hand one prompt to your agent The Setup screen has this prompt ready to copy. Give it to your coding agent with your project open, and name any other repos it should read. Claude Code, Codex, Cursor and any other agent that reads a SKILL.md file can run it. ```text Fetch and follow the instructions at https://barua.ai/setup.md to set up barua for this project. ``` The [setup guide](https://barua.ai/setup.md) tells your agent how to inspect the project and resume existing setup. It loads the detailed skill with `npx skills add getbarua/skills --skill barua-setup`. To install it for one agent without questions, add `-a claude-code` or `-a codex` and `-y` to that command. The skill is public at https://github.com/getbarua/skills, and your agent can also read it at https://barua.ai/skills/barua-setup/SKILL.md. ## What your agent does The skill gives your agent these steps and rules, and your agent follows them in your repos, on your machine: - It reads the repos you named and finds the line in your server code where each fact becomes true, such as the commit that saves a new user. It never fakes an event your product doesn't have. - It proposes the full event plan, even when the core events are all you need. It approves the exact version when delegated, or keeps an operation waiting for your review. - It writes the code that sends each event from your server once the database commit is done. It never puts that code in a browser. - It runs each flow through your product's own code, such as its test suite. Then it checks the events with the skill's verify script until every one passes, or until a step needs you. - It derives brand details and a business description from the repos. With management access it can confirm the initial brand and make requested later changes. It asks for facts the repos do not establish. - If your product deletes accounts, it adds a job that erases the person from barua too. - It adds a short block to your repo's agent instructions (`AGENTS.md`, which Claude Code reads through `CLAUDE.md`), so the next agent keeps the events right. It never writes through a symlink. When one of those files links somewhere else, it gives you the block to add there by hand. The skill tells your agent never to send barua your source code or environment secrets. It sends the declared events and specific configuration fields, including the brand and business description. It keeps a source for the product facts it uses. When your frontend does something your server never hears about, your agent reports it as `needs_server_call` with a suggested endpoint. Whether to add that endpoint is your call. ## The core events barua defines the same core events for every product, as `core@1`. Your server sends each one when its fact becomes true, with your own user id. - Required: `signed_up` - Declarable: `activated` `created` `purchased` `churned` - Optional: `consent_updated` `checkout_started` `order_shipped` `order_delivered` `order_delayed` `order_cancelled` `order_refunded` `trial_started` `trial_ending` `trial_ended` `subscription_started` `subscription_cancelled` `signed_in` `signed_up` is required. When your product has no source for a declarable event, such as `purchased` in a product that takes no payments, your tracking plan declares it `not_emitted` with a reason, and plays that need it stay off. Send an optional event whenever your product has its moment. Receipts and order updates come from `purchased` and the order events. When your server knows someone's time zone, send it on `signed_up` as `timezone`, such as `Europe/Oslo`. Without the pixel, that's the only place barua can learn it, and their marketing email otherwise waits for the send window in your workspace's time zone, or in UTC until you set one under Settings. Facts only your product has, such as a project going live, become custom events in your tracking plan, and barua accepts them once you approve the plan. Every property of every core event is in the full contract at https://barua.ai/docs/events.md, and `GET /api/integrations/v1/plan` returns your workspace's approved custom events with theirs. ## Choose what to review Every play needs an exact approval before it can send. You choose whether your agent can give it or whether it waits for you. Settings, Agents shows pending requests and their content. Approval applies to that action and version; a changed request needs a new decision. The API supports the same review without a browser. - The tracking plan defines your custom events and which core events your product does not emit. - The brand includes your business description. Once it is confirmed and the plan approved, barua starts writing the emails. Forwarding an existing email is optional and supplies its design. - Your sending domain needs verified DNS records. The agent can add them with your authorized DNS tool and check them through Barua. DNS changes can take a while, so start early. - Each play combines an event rule and its emails. You or your authorized agent inspect real renders and approve the exact languages. Marketing plays still follow the consent rule in the [privacy page](https://barua.ai/privacy). Receipts and order updates work the same way. barua writes their words once in your brand's voice. When it can't, the email uses barua's own fixed wording in English or Norwegian. The Plays map shows which one you're approving. A reader in a language you haven't approved gets the English email when English is approved, and otherwise your first approved language. Either way you approve them before any goes out. They skip the marketing consent check, because they are about an order the person placed. The order details in them, such as the total, come from the event your server sent. ## Go live Enable live intake through an authorized management connection. If you chose launch review, the request waits in Settings → Agents. Turn play sending on in Setup or through the same connection. A new workspace starts with sending off; approving templates does not turn it on. All current owners need current terms before live intake can start. Activate sending before the occurrence used for proof, because an event from before play emails are first on never starts a play. Then install the live key in production secrets and deploy. Start a fresh delivery proof for that key and an approved play revision, then trigger a new qualifying action through your live product. Barua follows its event through the play and provider delivery report. The agent reports whether it is waiting on a sending window or another condition. Provider acceptance alone does not establish delivery. Old setup stamps and duplicate events don't count either. The legacy verify script still checks live event intake; it does not replace this proof. From then on, barua sends when a real person matches a play you approved and, for a marketing play, the consent rule lets it reach them. On a quiet launch that can take a while. ## The pixel (optional) You can use server events without the tag. With pixel plays enabled for your workspace, site activity can also start marketing plays. The pixel captures page and product views, along with searches and cart activity. Barua uses those observations in the same play engine as your server events. The person still needs to be identified and eligible for marketing email. Your approved plan and plays remain required. Pixel activity does not establish a paid order or marketing permission. Your server remains the source for receipts and consent changes. The pixel also builds a profile from the markup your pages already publish. The rest of this page covers the tag and the calls it exposes. ## The tag ```html ``` Your key is on the Setup screen in the app, shown once when it's issued. Paste the tag high in the `` on every page. Capture starts immediately, and the first profiles fill within a couple of hours. ## The key is public by design The key ships in your HTML, so treat it as public; it's built for that. All it can do is post visitor activity into your workspace; it can never read anything back. Browser posts are checked against your site's origin once it's set in the app (a best-effort abuse check, never an authentication boundary), and the key is stored hashed on our side. Want a fresh one? Rotate it from Setup in one click. The old key is refused everywhere within a few minutes, and that includes the copy still sitting in your deployed HTML: paste the new key into the tag and redeploy right after rotating, or capture stops until you do. ## What it captures A masked replay of each visit, plus a stream of plain events read from the markup your site already publishes. Masking happens in the browser: every input value and every piece of rendered text is replaced before anything is sent, with one narrow stylesheet edge described on the privacy page. An iframe's contents are never recorded, and an element you mark with `data-cg-block` keeps only an empty placeholder in the replay. The full stance is on the [privacy page](https://barua.ai/privacy). The events: - Pages: `pageview` `page_title` `page_leave` `tab_visible` `attention` - Interaction: `click` `rage_click` `exit_intent` `form_abandon` `form_state` `site_search` `custom` - Commerce: `product_view` `add_to_cart_click` `cart_state` `datalayer` - Context: `attribution` `device` `geo` `js_error` - Identity: `identify` One detail worth knowing: on a shop whose own cart endpoint doesn't answer, barua infers the cart from add-to-cart clicks and marks it as inferred, so downstream decisions know the difference. Cart events distinguish a confirmed cart from an estimate or an unavailable reading. A confirmed empty cart clears the previous contents. Product details can refresh when your page updates its structured markup. Form events distinguish an attempt from a confirmed completion. They include field types and browser validation categories. A submitted form with an unknown result stays unresolved. Clicks include the destination link when one exists, and attention is saved when a tab moves into the background. ## Tag attributes | Attribute | What it does | | --- | --- | | `data-key` | Required. Your workspace key: `pk_` followed by 48 hex characters. | | `data-consent` | Set to `"required"` to capture nothing, and write nothing to the device, until your consent banner calls `barua.consent(true)`. A refusal stores only the refusal marker. The mode for an EU posture. | | `data-auto-identify` | Set to `"off"` to turn off automatic email pickup. Explicit `barua.identify()` calls still work. | | `data-endpoint` | Overrides where event batches post, for proxying through your own domain. The handed-out snippet doesn't need it, and the once-per-session city lookup still calls barua.ai directly. | | `data-cg-block` | This one goes on any element of your page. Its contents are never recorded; the replay keeps an empty placeholder carrying the element's tag, class, width, and height. | ## Serve it from your own domain The tag works without this section: as handed out, it captures today, and as of August 2026 the common blockers' filter lists do not carry barua, so their domain rules have nothing to match. This section is for the day some list adds it. Most ad blockers block by domain. Serve the tag and its events from your own domain and there is no third-party domain left for a filter list to match. Consent is a separate promise, and the proxy changes nothing about it. The rewrite routes live in your hosting configuration, with no DNS changes. Your sending domain setup stays exactly as it is. The one exception is the Cloudflare Worker path, which assumes your site already runs through Cloudflare; if it does not, use the rewrite route your own host provides instead. The script itself sends event batches with credentials omitted, leave-time flushes included, so proxied batches carry no site cookies on any host; the Worker and Nginx recipes also strip the header outright. What remains on plain-rewrite hosts is the script file request, plus the rare leave-time flush too large for a credential-free send, whose cookies travel in transit only: barua's event intake ignores cookies and stores none. Your setup page shows the routes ready to paste. Netlify and Vercel take two rewrite lines each. Cloudflare takes a small Worker on the same two paths, and any proxy that can forward two paths works the same way: one serves the script from a path on your site, the other forwards event batches. The tag then uses `data-endpoint` with that path. Pick short, bland paths. Words like analytics, stats, track, or pixel get matched by filter patterns, and blockers running in stricter modes can still match paths, which is exactly why the names matter. The proxy changes nothing about consent: the script still captures nothing before `barua.consent(true)`. One optional line of server middleware goes further: when a request URL carries the `b=` email token, report it, and the click is captured even in a browser where the script never ran. The exact call is on your setup page. This runs in your server, outside the script's consent gate, and it stores one scrubbed URL of a barua-tagged click. The row names no person and no session, and the token in the address links it to the email it came from. If your consent posture requires it, fire it only when your own consent cookie grants. It is your middleware, and that one line is yours to gate. ## The JavaScript API The tag exposes `window.barua` with four calls. ### barua.identify(email, externalId?, traits?) Usually you never call it: when a visitor gives your site their email through a popup or a checkout, barua picks it up. For a logged-in app, call it when each page restores a signed-in session or changes account: ```js barua.identify('them@theiremail.com') ``` The sign-in event itself lands after the async tag has loaded in practice. A call that can run at page boot instead (a restored session) gets the same guard as consent: `window.barua?.identify(…)`, repeated when your session state settles. On a consent-required site, an identify fired before the visitor opts in is held in memory and delivered when `barua.consent(true)` arrives. `traits` carries what no form captured, an OAuth signup's name for instance. Recognized keys: `first_name`, `last_name`, `name`, `phone`. Values are capped at 80 characters, and any other key is dropped. `externalId` is your own user id. barua keeps it as unverified evidence and never trusts it to say who someone is: a browser can claim to be anyone. To make an id stick, send it from your server as the user id on a `signed_up` sent with your live key. Create that secret integration key in Setup or through an authorized management connection (see Connect your agent). Once that signup applies, your later events with the same id reuse it. Enable live intake through an authorized management connection after setup checks pass (see Go live). A different identity starts a fresh browser context. Activity already queued keeps its original owner. A SHA-256 marker allows the tag to notice identity changes across page loads without storing the email address in that marker. A page that has identified someone keeps that account until its own next `identify` or `reset` call. Unidentified tabs can adopt the latest shared browser context. If the browser cannot coordinate the first identity claim safely, earlier anonymous activity may remain separate from the identified profile. ### barua.reset() Call this when the page learns that someone has signed out. It starts fresh anonymous and session IDs. Other unidentified tabs on the same site adopt the change; pages that identified their own account follow their own sign-in state. Already queued activity keeps its original IDs. Use `barua.consent(false)` to stop collection and clear pending activity. ### barua.track(name, properties?) Use this when your app knows a meaningful action happened. For example: `barua.track('feature_used', { content_type: 'editor', count: 1 })`. Recognized properties accept short codes or numbers; unknown properties are dropped. Do not pass form answers or free text. Call once for each action. The tag reuses its generated event ID when it retries delivery. Supported short-code fields are `form_id`, `attempt_id`, `step`, `step_id`, `status`, `result`, `method`, `plan`, `plan_id`, `content_id`, `content_type`, `item_id`, and `variant_id`. Use the same `step_id` and item ID when different event names describe one operation's progress and outcome. Supported numeric fields are `step_index`, `count`, `duration_ms`, and `value`; `currency` accepts a three-letter code. ### barua.consent(ok) For sites running `data-consent="required"`. Wire it to your consent banner: `barua.consent(true)` starts capture, and `barua.consent(false)` stops the recorder, clearing both the queue and the pixel's stored values; the only thing a refusal keeps is the refusal marker itself. Calling it repeatedly is safe. The pixel loads async, so a banner that can fire before it's ready should guard the call (`window.barua?.consent(true)`) and repeat it on the consent tool's change event. Once a choice is stored, later pages need no call at all: the pixel reads it itself on load. ## If your visitors are in Europe, read this one By default the tag captures from the first pageview and writes its identifiers to the browser's localStorage. Unconfirmed activity batches expire after 24 hours, within a 1 MiB limit, and are removed after the server confirms them. Expired batches are deleted the next time the tag is allowed to run. Replay is excluded from this saved queue. That default is right for plenty of sites and wrong for some, and which one you are is a legal question rather than a technical one. Across the EEA and the UK, putting anything on a visitor's device or reading it back needs their consent first, unless it is strictly necessary for something they asked for. Local storage counts, the same as a cookie: European regulators settled that question explicitly, and the fact that barua sets no cookies does not exempt it. So if your visitors are there, run consent mode: ```html ``` Nothing is captured and nothing is written to the device until your banner calls `barua.consent(true)`. Wire that call to whatever consent tool you already run, and call `barua.consent(false)` when someone withdraws, which clears the stored values and keeps only the refusal. The second switch is about contact details. barua picks up an email or a name at the moment a form or a popup actually sends, by default. That is a real collection made by you, and your own notice has to cover it. If it does not, or if you would rather hand identities over from your own code, turn the automatic pickup off: ```html ``` One more thing worth a minute of thought. barua masks every input value and every piece of rendered text in the browser, but it deliberately keeps the context it needs to be useful: page titles, product names and prices, and what someone searched for on your site. On most sites that is harmless. If a page title would say that a person was reading about a medical condition or a religion, that is a different category of data with a much higher bar. **Blocking the visible element is not enough for a title.** The pixel drops a page title only when the `` element itself, or something above it, carries the attribute, so a page that hides its body but leaves its title alone still sends the title. For a route like that, mark the title: ```html <title data-cg-block>Your order ``` Or keep the tag off the route entirely, which is the safer answer when a whole section of the site is sensitive. `data-cg-block` on a body element still does its own job: the contents of that element are never recorded. The [acceptable use policy](https://barua.ai/acceptable-use) is where this is spelled out, and the [privacy page](https://barua.ai/privacy) has the full detail of what is masked. ## Single-page apps Nothing extra to do. The pixel follows `pushState` and `popstate` navigations, and corrects the page title after your router settles. Call `identify` when a page restores or changes its signed-in account, and `reset` when it learns of a sign-out. ## Getting access Access is a conversation today. Write chris@barua.ai, and a person answers. --- Source: https://barua.ai/docs/events.md (also https://barua.ai/skills/barua-setup/references/events.md) # Barua events: the contract for your backend Your backend tells Barua what happened in your product, one event per fact, sent from server code after the database commit. This page is that contract. `GET /api/integrations/v1/plan` returns the same contract as JSON, with a JSON Schema for one event, and `GET /api/integrations/v1/events/status` tells you what arrived. Every path below is under `https://barua.ai/api/integrations/v1` and takes `Authorization: Bearer ik_...`, an integration key your workspace owner or authorized management connection creates. Requests and answers are JSON. For setup, approval and ongoing control, use the separate [management API](https://barua.ai/skills/barua-setup/references/management.md) with a `mk_` key. For each call below, only the answer in this table means it went through. Treat any other answer as a failure, another 2xx included: an unlisted 200 or 204 came from something between you and Barua, such as a proxy, or from a wrong `BARUA_API_URL`. | Call | Success | | --- | --- | | `POST /events` | 202 with `receipt_id` and `records` | | `POST /people/erase` | 200 with `"erased": true` | | `POST /plan` | 201 for a new proposal, 200 for a plan Barua already has, each with `version` and a `"status"` of `"proposed"` or `"approved"` | | `PUT /brand` | 200 with `"status": "proposed"` | | Every `GET` | 200 | ## Keys Keys are `test` or `live`. A test key runs the same checks on each event as the live key and reports the verdict: `validated` when it passes, or `quarantined` with its reasons. It stops there and changes nothing about your users in Barua. Its checks never read the people and events the live key sent, so an event the test key validated can still get `duplicate` or `unknown_identity` from the live key. Use the test key in development and CI. The live key lives in production's hosting secrets. An agent can install it through an already authorized secret tool without printing it. An owner or authorized management connection enables live events after setup checks pass. Until they're on for yours, a live key gets 403 `feed_not_enabled`. A test key works either way. Erasing a person needs a live key; a test key gets 403 `test_key_not_allowed`. New keys created by a management connection have explicit capabilities. `events:write` admits current events. `history:write` is additionally required for backfill and live events older than 72 hours. Accepted retries retain their original key and idempotency key, even after the event ages past that threshold. Customer lookup needs `people:read`; erasure needs `people:erase`. Restricted keys can read only their own receipts and cannot propose plans or brands; use management actions for those. Existing keys without a capability list keep their prior behavior. Revoking a management connection leaves deployed event keys working, so revoke those separately when intended. ## Emitter rules - Send after the database commit, from a job queue or the platform's `waitUntil`, else await the send before you respond. The last fallback applies only without a durable queue or outbox. With a queue or outbox, commit the fact and event together and return the product response without awaiting a worker, flush, drain or delivery. The worker awaits HTTP outside database transactions and records the result afterward. On a serverless or edge platform, an unowned send still running after the response can die with the invocation, and its failure log dies with it. A `consent_updated` that turns marketing off goes only through a job queue or an outbox table, because a lost one leaves Barua treating someone who said no as opted in. - Retry 429 and 5xx answers, and a send that got no answer at all: a network failure, or no answer within 10 seconds. Barua drops duplicates by `event_id`, so a resend is safe even when the first send arrived. Send the same body each time, up to 5 attempts in all, or fewer to fit the time the send gets. Wait 0.5 seconds before the second attempt and double the wait before each one after it, with random jitter of up to half the wait either way. When an answer carries `Retry-After` as a whole number of seconds, wait that long instead. Never wait more than 30 seconds between attempts, even when `Retry-After` asks for longer. - Never retry any other answer. A 4xx other than 429 fails the same way every time, so log it and act on its `fix`. - A send went through only when Barua answers 202 with `receipt_id` and `records`. Treat any other answer as a failed send and log its status. That includes another 2xx. - Use the test key everywhere except production. - Never send from the browser. - Log a send that still fails after its retries, with the event's name and `event_id` and never the key. Barua can't see an event that never arrived, so your log is its only trace. The skill's `references/emit.ts` sends and retries this way for Node, Deno, Bun and Workers. It never throws: it resolves `{ok, status, body}`, so your code checks `ok` and logs a failure. Its `apiUrl` must be https, or plain http to localhost. For another stack, send the HTTP call under [Send events](https://barua.ai/skills/barua-setup/references/events.md#send-events). ## The event | Field | Rule | | --- | --- | | `event_id` | Required. `name:id`, for example `created:4812`, where the id is the source row's id. 1 to 512 characters after the colon, with no control characters, so any id up to 256 characters fits with room for an occurrence counter. When `GET /plan` gives the event an `id_from`, the id is the value it names (`purchased` uses `properties.order_id`). An `id_from` that names two values joins them with a colon: `checkout_started::`. Only the first value may contain a colon, so each pair makes its own id. Three events build a part from a value. `trial_ending` ends with `trial_ends_at` in Unix milliseconds. `signed_in` ends with the UTC date of `occurred_at`, as YYYY-MM-DD. `order_delayed` ends with its `eta_date`, or the word `unknown` when it has none. Either way, two rows never share an `event_id`. A repeatable event gets one id per occurrence. | | `name` | Required. One of the core events below, or a custom event in your approved tracking plan. | | `external_id` | Required. Your user id for this person, 1 to 256 characters, counted the way the identities feed counts them: an emoji counts as 2. | | `occurred_at` | Required. The source row's timestamp, in ISO 8601 with a timezone. It must be after 2000 and at most 5 minutes after Barua receives the event, so send the time the row records. | | `properties` | Optional. An object with the event's properties below, at most 8 KiB as JSON. A `null` optional property counts as absent. | Every workspace sends `signed_up`. `activated`, `created`, `purchased` and `churned` are declarable: when your product has no such moment, your tracking plan marks the event `not_emitted` with a reason. The rest are optional, from `consent_updated` on, so the plan can't declare them. Your plan can't list an optional event either, so Barua suggests plays that start from one only after it has accepted one sent with your live key. Events sent with a test key don't count. | Event | Send it | What it means | | --- | --- | --- | | `signed_up` | required | Someone created an account. Send it once, after the user row commits. `event_id` is `signed_up:`. | | `activated` | declarable | Someone reached the first real result in your product. You choose the moment. `event_id` is `activated:`, one per occurrence. | | `created` | declarable | Someone made the main thing your product exists for, such as a project. One event per created row. `event_id` is `created:`. | | `purchased` | declarable | A payment succeeded. `amount` is the total charged, after discounts, including shipping and tax. `event_id` is `purchased:`. | | `churned` | declarable | Someone's subscription or relationship with your product ended. `event_id` is `churned:`, one per occurrence. | | `consent_updated` | optional | Someone changed their marketing consent. `event_id` is `consent_updated:`, one per change. | | `checkout_started` | optional | Someone started a checkout your server created, such as a Stripe Checkout Session. `event_id` is `checkout_started::`, one per start. | | `order_shipped` | optional | A shipment of an order left for delivery. An order sent in two parcels sends two. `event_id` is `order_shipped:`. | | `order_delivered` | optional | A shipment was delivered. `occurred_at` is the delivery time. `event_id` is `order_delivered:`. | | `order_delayed` | optional | A shipment's delivery moved later. `event_id` is `order_delayed::`, or `order_delayed::unknown` when there is no new date. | | `order_cancelled` | optional | An order was cancelled. Money paid back is an `order_refunded`. `event_id` is `order_cancelled:`. | | `order_refunded` | optional | Money from an order went back to the buyer. One event per refund. `event_id` is `order_refunded:`. | | `trial_started` | optional | Someone started a trial. `event_id` is `trial_started:`. | | `trial_ending` | optional | Someone's trial ends soon. Send it when the reminder is due. `event_id` is `trial_ending::`. | | `trial_ended` | optional | Someone's trial ended, including one that turned into a paid plan. `event_id` is `trial_ended:`. | | `subscription_started` | optional | Someone started a subscription. `event_id` is `subscription_started:`. | | `subscription_cancelled` | optional | Someone asked to cancel a subscription that is still running. `event_id` is `subscription_cancelled:`. | | `signed_in` | optional | Someone signed in. Send only their first sign-in of each UTC day. `event_id` is `signed_in::`, the UTC date of `occurred_at`. | `use` says where a value may go. A `copy` value may appear in an email Barua writes, such as a project title. A `targeting` value only decides who gets an email and when. | Property | Type | Required | Use | Min | Max | | --- | --- | --- | --- | --- | --- | | `signed_up.email` | email | yes | targeting | | 320 | | `signed_up.first_name` | string | no | copy | | 100 | | `signed_up.marketing_opt_in` | boolean | no | targeting | | | | `signed_up.type` | string | no | targeting | | 32 | | `signed_up.timezone` | timezone | no | targeting | | 64 | | `signed_up.locale` | locale | no | targeting | | 35 | | `created.id` | string | yes | targeting | | 256 | | `created.title` | string | yes | copy | | 500 | | `created.kind` | string | no | copy | | 64 | | `created.url` | url | no | copy | | 2048 | | `purchased.order_id` | string | yes | targeting | | 256 | | `purchased.amount` | number | yes | targeting | 0 | | | `purchased.currency` | currency | yes | targeting | | | | `purchased.line_items` | array | no | targeting | | 15 | | `purchased.line_items[].product_id` | string | no | targeting | | 256 | | `purchased.line_items[].name` | string | yes | copy | | 200 | | `purchased.line_items[].quantity` | integer | yes | targeting | 1 | | | `purchased.line_items[].price` | number | yes | targeting | 0 | | | `purchased.line_items[].url` | url | no | copy | | 2048 | | `purchased.line_items[].image_url` | url | no | copy | | 2048 | | `purchased.subtotal` | number | no | targeting | 0 | | | `purchased.discount` | number | no | targeting | 0 | | | `purchased.shipping` | number | no | targeting | 0 | | | `purchased.tax` | number | no | targeting | 0 | | | `purchased.coupon` | string | no | targeting | | 64 | | `purchased.checkout_id` | string | no | targeting | | 256 | | `purchased.receipt_url` | url | no | copy | | 2048 | | `purchased.order_name` | string | no | copy | | 100 | | `churned.reason` | string | no | targeting | | 200 | | `consent_updated.marketing_opt_in` | boolean | yes | targeting | | | | `consent_updated.method` | string | no | targeting | | 64 | | `checkout_started.checkout_id` | string | yes | targeting | | 256 | | `checkout_started.start_id` | string | yes | targeting | | 255 | | `checkout_started.amount` | number | yes | targeting | 0 | | | `checkout_started.currency` | currency | yes | targeting | | | | `checkout_started.checkout_url` | url | no | copy | | 2048 | | `checkout_started.line_items` | array | no | targeting | | 15 | | `checkout_started.line_items[].product_id` | string | no | targeting | | 256 | | `checkout_started.line_items[].name` | string | yes | copy | | 200 | | `checkout_started.line_items[].quantity` | integer | yes | targeting | 1 | | | `checkout_started.line_items[].price` | number | yes | targeting | 0 | | | `checkout_started.line_items[].url` | url | no | copy | | 2048 | | `checkout_started.line_items[].image_url` | url | no | copy | | 2048 | | `order_shipped.order_id` | string | yes | targeting | | 256 | | `order_shipped.shipment_id` | string | yes | targeting | | 256 | | `order_shipped.order_name` | string | no | copy | | 100 | | `order_shipped.carrier` | string | no | targeting | | 60 | | `order_shipped.tracking_number` | string | no | targeting | | 100 | | `order_shipped.tracking_url` | url | no | targeting | | 2048 | | `order_shipped.eta_date` | date | no | targeting | | | | `order_shipped.line_items` | array | no | targeting | | 15 | | `order_shipped.line_items[].product_id` | string | no | targeting | | 256 | | `order_shipped.line_items[].name` | string | yes | copy | | 200 | | `order_shipped.line_items[].quantity` | integer | yes | targeting | 1 | | | `order_shipped.line_items[].price` | number | yes | targeting | 0 | | | `order_shipped.line_items[].url` | url | no | copy | | 2048 | | `order_shipped.line_items[].image_url` | url | no | copy | | 2048 | | `order_delivered.order_id` | string | yes | targeting | | 256 | | `order_delivered.shipment_id` | string | yes | targeting | | 256 | | `order_delivered.order_name` | string | no | copy | | 100 | | `order_delivered.carrier` | string | no | targeting | | 60 | | `order_delivered.tracking_number` | string | no | targeting | | 100 | | `order_delivered.tracking_url` | url | no | targeting | | 2048 | | `order_delivered.line_items` | array | no | targeting | | 15 | | `order_delivered.line_items[].product_id` | string | no | targeting | | 256 | | `order_delivered.line_items[].name` | string | yes | copy | | 200 | | `order_delivered.line_items[].quantity` | integer | yes | targeting | 1 | | | `order_delivered.line_items[].price` | number | yes | targeting | 0 | | | `order_delivered.line_items[].url` | url | no | copy | | 2048 | | `order_delivered.line_items[].image_url` | url | no | copy | | 2048 | | `order_delayed.order_id` | string | yes | targeting | | 256 | | `order_delayed.shipment_id` | string | yes | targeting | | 256 | | `order_delayed.order_name` | string | no | copy | | 100 | | `order_delayed.carrier` | string | no | targeting | | 60 | | `order_delayed.tracking_number` | string | no | targeting | | 100 | | `order_delayed.tracking_url` | url | no | targeting | | 2048 | | `order_delayed.eta_date` | date | no | targeting | | | | `order_delayed.line_items` | array | no | targeting | | 15 | | `order_delayed.line_items[].product_id` | string | no | targeting | | 256 | | `order_delayed.line_items[].name` | string | yes | copy | | 200 | | `order_delayed.line_items[].quantity` | integer | yes | targeting | 1 | | | `order_delayed.line_items[].price` | number | yes | targeting | 0 | | | `order_delayed.line_items[].url` | url | no | copy | | 2048 | | `order_delayed.line_items[].image_url` | url | no | copy | | 2048 | | `order_cancelled.order_id` | string | yes | targeting | | 256 | | `order_cancelled.order_name` | string | no | copy | | 100 | | `order_cancelled.reason` | string | no | targeting | | 200 | | `order_cancelled.amount` | number | no | targeting | 0 | | | `order_cancelled.currency` | currency | no | targeting | | | | `order_refunded.order_id` | string | yes | targeting | | 256 | | `order_refunded.refund_id` | string | yes | targeting | | 256 | | `order_refunded.amount` | number | yes | targeting | > 0 | | | `order_refunded.currency` | currency | yes | targeting | | | | `order_refunded.full` | boolean | yes | targeting | | | | `order_refunded.reason` | string | no | targeting | | 200 | | `order_refunded.line_items` | array | no | targeting | | 15 | | `order_refunded.line_items[].product_id` | string | no | targeting | | 256 | | `order_refunded.line_items[].name` | string | yes | copy | | 200 | | `order_refunded.line_items[].quantity` | integer | yes | targeting | 1 | | | `order_refunded.line_items[].price` | number | yes | targeting | 0 | | | `order_refunded.line_items[].url` | url | no | copy | | 2048 | | `order_refunded.line_items[].image_url` | url | no | copy | | 2048 | | `trial_started.trial_id` | string | yes | targeting | | 256 | | `trial_started.trial_ends_at` | timestamp | yes | targeting | | | | `trial_started.plan` | string | no | copy | | 64 | | `trial_ending.trial_id` | string | yes | targeting | | 256 | | `trial_ending.trial_ends_at` | timestamp | yes | targeting | | | | `trial_ending.plan` | string | no | copy | | 64 | | `trial_ended.trial_id` | string | yes | targeting | | 256 | | `trial_ended.plan` | string | no | copy | | 64 | | `subscription_started.subscription_id` | string | yes | targeting | | 256 | | `subscription_started.plan` | string | no | copy | | 64 | | `subscription_cancelled.subscription_id` | string | yes | targeting | | 256 | | `subscription_cancelled.plan` | string | no | copy | | 64 | | `subscription_cancelled.reason` | string | no | targeting | | 200 | | `subscription_cancelled.ends_at` | timestamp | no | targeting | | | Min is the smallest value a number or integer may take, and `> 0` means it must be above 0. A line's `quantity` is a whole number of at least 1. No amount is below 0. An amount can be 0, such as an order a discount pays in full, except a refund's `amount`, which is above 0 because a refund of 0 moved no money. Max is a length in characters, or for an array the most entries it holds. An array is a list of objects, one per line of the order, checkout, shipment or refund. A `date` is one calendar day written `YYYY-MM-DD`, such as `2026-10-12`. It has no time, and it must be a real day. A `timezone` is a time zone name the IANA database knows, such as `Europe/Oslo` or `UTC`. Barua refuses an offset such as `+01:00`. A `locale` is a BCP 47 language tag, such as `en-US` or `zh-Hant-TW`. Its own `use` is `targeting`, because Barua never puts a whole list in an email; each entry field has its own `use`. Required on an entry field means every entry has it. `checkout_started.start_id` can't contain a colon. It is the second part of the `event_id`, after `checkout_id`, which may contain colons, so each pair makes its own id. When `start_id` is the start's time, send it in Unix milliseconds, because an ISO time has colons. Strings are trimmed and can't be empty or hold control characters. Emails are lowercased. A negative amount is quarantined as `invalid_property`, and a refund is not a purchase. `purchased.amount` is the total charged. Discounts are already taken off it, and shipping and tax are in it. `subtotal`, `discount`, `shipping` and `tax` are optional parts of it. Every amount, a line's `price` included, is in major units (19.99 for $19.99) and can't be negative, and `currency` is an ISO 4217 code in capitals, such as `USD`. A line's `price` is the unit price. Stripe's `unit_amount` counts in Stripe's charge unit (1099 for $10.99), so convert it first. Never send it as it comes. `checkout_started.amount` is the checkout's total, counted the same way. It always comes with its `currency`. `line_items` holds at most 15 lines. An order with more lines sends the first ones up to that cap, and `amount` still covers the whole order. A full list, each line with a product link and an image link, plus every other `purchased` property, comes to about 6 KiB of the 8 KiB `properties` cap. Over the cap Barua quarantines the whole event, so send what a receipt needs and nothing more. A reason about one line names it, such as `properties.line_items[2].price`. `amount` and `currency` come together. An event that has both takes both or neither. An `order_cancelled` sent before any money moved leaves both out. One half alone is quarantined as `missing_property` for the other. Send one order event per status, from the server code that records it. `order_id` is the `order_id` of the order's `purchased`. A shipment is one parcel, so an order sent in two parcels sends two `order_shipped`, each with its own `shipment_id`. `order_delivered` and `order_delayed` repeat the shipment's facts, so each one says which parcel it is about. Send the carrier, tracking number, tracking link and delivery date as your shipping system has them. Barua's shipment emails show them as you send them, and the tracking link becomes the email's button, so send the link the customer follows. A delay with a new date gets its own `event_id`, so each new date is a new event. A shipment gets one delay without a date. `order_refunded.amount` is what this refund paid back, in major units. A refund is its own event, so its amount is above 0. Barua quarantines a refund of 0 as `invalid_property`. A refund that moved no money, such as one that only puts items back in stock, sends nothing. Send `full: true` when nothing paid is left on the order, including when this refund is the last of several. Send `false` for a partial refund. Barua stores each refund. A full refund also ends the order's paid support, the way a purchases-feed refund does: the order stops counting as paid and stays in the buyer's history. A partial refund changes nothing else. Neither does a full refund whose `occurred_at` isn't later than the order's last update, such as one stamped the same second as its `purchased`. A full refund for an order Barua hasn't seen records that order as refunded. It counts in the buyer's history and doesn't count as paid. Once the owner approves the refund email, each refund also gets one email, which says the amount and whether the order was refunded in full, read from `full`. A model words Barua's receipt and order update emails once per workspace and language, from the brand pack and playbook, and never sees an order; Barua's code fills in every order detail from the event's own properties: `order_name` (else `order_id`), `amount` with `currency`, each line's `name` and `quantity` with its unit `price`, `receipt_url`, `tracking_url`, `carrier`, `tracking_number`, `eta_date`, `full` and `reason`. A shipment's lines show no price, because a shipment event carries no currency. The email's only link is its button: the `receipt_url` or `tracking_url` the event carried, else the website from the brand pack. Barua refuses any other link, so a receipt hosted by a payment provider works when it arrives as `receipt_url`. None of these emails sends until the workspace owner or an agent authorized under their policy approves it. After that it goes out without the `marketing_opt_in` check, usually within a few hours of the event. One that can't go out within a day of the event's `occurred_at` is dropped, so send these events as they happen. Send `trial_ending` when the reminder is due, such as three days before the trial ends. A play only acts at or after a time it reads, so it can't send that reminder from `trial_ends_at` alone. Its `event_id` holds `trial_ends_at` in Unix milliseconds, so when a trial is extended, the reminder for the new end is a new event. `subscription_cancelled` is the request to cancel, sent while the subscription still runs, so a play can try to keep the customer. `churned` is the end. A subscription gets one `subscription_cancelled`: a second request after the customer undid the first comes back `event_id_conflict`. `subscription_started` has no amount, because each paid invoice is a `purchased`. Send `signed_in` for someone's first sign-in of each UTC day only. Barua keeps each event as a row until the person is erased, and a play that asks when someone was last seen needs only the latest day. A later sign-in that day has the same `event_id`, so it comes back `event_id_conflict` and adds no row. `signed_up.type` says how someone joined, such as `organic` or `invited`. `timezone` is an IANA time zone such as `Europe/Oslo`, and `locale` is a BCP 47 language tag such as `en-US`. Barua checks both. A value it can't read, such as `Oslo` or `en_US`, quarantines the whole `signed_up` as `invalid_property`, so leave one out unless it comes from a source that writes these formats, such as the browser's `Intl.DateTimeFormat().resolvedOptions().timeZone` and `navigator.language`. Rules can read them now. When the pixel hasn't given Barua someone's time zone, their marketing email waits for its send window in this `timezone`, so send it when your server knows it. The language Barua writes in doesn't read `locale` yet. A receipt or an order update writes its amounts and dates the way the person's `locale` does. `consent_updated.method` records how the choice was made, such as `signup_form` or `settings`. It is a record for you, and no play reads it. ## Events the product already sends elsewhere When the product already sends one of these to another tool, send Barua the event on the right from the same place. Every rule on this page still applies to it. `references/reading-repos.md` has the full Stripe payment rules and `stripeAmount`, which converts Stripe amounts. Stripe gives every time in Unix seconds, such as a subscription's `trial_end` and a refund's `created`. Barua wants ISO 8601 text with a time zone, so convert each one with `new Date(seconds * 1000).toISOString()` before it goes into a timestamp property or `occurred_at`. Barua quarantines a number sent as it comes. Check for null first, because `new Date(null * 1000)` is a time in 1970 and no error. Stripe gives every amount in its charge unit, so convert each one through `stripeAmount`. | Already sent | Send Barua | | --- | --- | | Segment `Order Completed` | `purchased`. Its `order_id` is the `order_id`, and its `total` and `currency` are the `amount` and `currency`. | | GA4 `purchase` | `purchased`. Its `transaction_id` is the `order_id`, and its `currency` is the `currency`. Take `amount` from the order's own total, because `value` leaves out shipping and tax. | | Klaviyo `Placed Order` | `purchased`. Its `OrderId` is the `order_id`, and its `value` and `value_currency` are the `amount` and `currency`. | | Stripe `invoice.paid`, when `amount_paid` is above 0 | `purchased`. The invoice's `id` is the `order_id`, and its `amount_paid` and `currency` give `amount` and `currency` through `stripeAmount`. | | Stripe `checkout.session.completed`, when `mode` is `payment` and `payment_status` is `paid` | `purchased`. The session's `id` is the `order_id`, and its `amount_total` and `currency` give `amount` and `currency` through `stripeAmount`. Send nothing when either is null. | | The server code that creates a Stripe Checkout Session, when its `amount_total` and `currency` aren't null | `checkout_started`. The paragraph below sets `checkout_id`, `start_id`, `amount` and `currency`. | | Stripe `customer.subscription.created`, when `status` is `active` or `trialing` | `subscription_started`. The subscription's `id` is the `subscription_id`. A subscription that starts `incomplete` sends it later, from the `customer.subscription.updated` whose `previous_attributes.status` is `incomplete` and whose `status` is now `active`. | | Stripe `customer.subscription.created`, when `status` is `trialing` | `trial_started`, beside the `subscription_started`. The subscription's `id` is the `trial_id`, and `trial_ends_at` is `new Date(trial_end * 1000).toISOString()`. Send nothing when `trial_end` is null. | | Stripe `customer.subscription.trial_will_end`, when `status` is `trialing` and `trial_end` is still ahead | `trial_ending`. The subscription's `id` is the `trial_id`, `trial_ends_at` is `new Date(trial_end * 1000).toISOString()`, and the `event_id` ends with `trial_end` times 1000, its Unix milliseconds. Stripe also sends this event at once when a trial is ended early, and then it isn't a reminder, which is why the condition checks both. Send nothing when `trial_end` is null. | | Stripe `customer.subscription.updated`, when a cancel is now pending | `subscription_cancelled`. The subscription's `id` is the `subscription_id`. The paragraph below says when a cancel is pending. | | Stripe `customer.subscription.deleted` | `churned`. The subscription has ended, so it is never a `subscription_cancelled`. | | Stripe `customer.subscription.updated`, when `previous_attributes` names `status` as `trialing` and `status` is no longer `trialing` | `trial_ended`. The subscription's `id` is the `trial_id`, and `occurred_at` is `new Date(trial_end * 1000).toISOString()`. Send nothing when `trial_end` is null. | | Stripe `customer.subscription.deleted`, when `trial_end` isn't null | `trial_ended`, beside the `churned`. The subscription's `id` is the `trial_id`. `occurred_at` is `new Date(trial_end * 1000).toISOString()`, or `ended_at` when that is earlier, converted the same way, because a trial cancelled before its end ended then. | | Stripe `refund.created` when the refund's `status` is `succeeded`, or the `refund.updated` that moves a pending refund to `succeeded` | `order_refunded`. The refund's `id` is the `refund_id`, and its `amount` and `currency` give `amount` and `currency` through `stripeAmount`. `full` is the `refunded` of the charge it refunds. `order_id` is the id your `purchased` used for that payment. Send nothing when the refund's `charge` is null. | | Shopify's `fulfillments/create` webhook | `order_shipped`. The fulfillment's `id` is the `shipment_id`, and `order_id` is the id your `purchased` used for that order. `carrier` is its `tracking_company`, while `tracking_number` and `tracking_url` keep their names. Leave out each one that is null. | | Shopify's `fulfillment_events/create` webhook, when `status` is `delivered` | `order_delivered`. The event's `fulfillment_id` is the `shipment_id`, `order_id` is the id your `purchased` used for that order, and `occurred_at` is its `happened_at`. | | Shopify's `fulfillment_events/create` webhook, when `status` is `delayed` | `order_delayed`. `shipment_id` and `order_id` come as for `order_delivered`. `eta_date` is the date part of `estimated_delivery_at`, left out when it is null. | | Shopify's `orders/cancelled` webhook | `order_cancelled`. `order_id` is the id your `purchased` used for that order, and `order_name` is its `name`. `occurred_at` is its `cancelled_at`. `amount` is its `total_price` as a number, with its `currency`. `reason` is its `cancel_reason`, left out when it is null. | Shopify's rows follow its webhook payloads (https://shopify.dev/docs/api/webhooks/latest). Shopify gives times as ISO 8601 text already, and amounts in major units as text, such as `"404.95"`, so only turn an amount into a number. An analytics call can arrive without a value Barua requires, because those specs leave most of their values optional. Take a missing value from the order the call describes. When the order has no total or no currency, send no `purchased`, since Barua quarantines one without them. Stripe sends no webhook when a Checkout Session is created, so send `checkout_started` right after `stripe.checkout.sessions.create` returns. Stripe leaves a session's `amount_total` and `currency` null when it has no total, such as a session in `setup` mode that only saves a card. Send `checkout_started` only when both have a value. A session without them isn't a checkout with a known total, so skip it and send nothing in its place. `checkout_id` is the session's `id` and `checkout_url` its `url`, left out when Stripe gives none. `amount` and `currency` come from the session's `amount_total` and `currency` through `stripeAmount`. When the product makes a new session for every attempt to pay, `start_id` is the session's `id` as well. When it keeps one session while the cart changes, `start_id` is the start's `occurred_at` in Unix milliseconds, recorded once when the start happens: each start then gets its own `event_id`, and a resend of one start repeats it. Send the same `checkout_id` on the `purchased` that completes the session. Lines come from `stripe.checkout.sessions.listLineItems(session.id, { limit: 15 })`. For each line, `name` is its `description` and `quantity` its `quantity`. `product_id` is its `price.product`, left out when the line's `price` is null. `price` is its `amount_subtotal` divided by its `quantity`, converted with `stripeAmount`. Never send `price.unit_amount` as it comes. Stripe can leave a line's `description` or `quantity` null, and every line needs both, so leave that line out. `amount` still covers the whole order. On the `purchased` from a completed session, `subtotal` is the session's `amount_subtotal`, and `discount`, `shipping` and `tax` come from `total_details.amount_discount`, `amount_shipping` and `amount_tax`, each converted the same way. Leave `subtotal` out when `amount_subtotal` is null, and the other parts out when `total_details` is null, because `stripeAmount` turns a null into 0. A cancel is pending when `previous_attributes` names `cancel_at_period_end` and it is now `true`, or names `cancel_at` and the subscription's `cancel_at` now holds a date. Skip every other update, such as one that sets them back, which is how a customer undoes a cancel. The rule holds whether or not the subscription has a `schedule`: a schedule that ends with a cancel sets `cancel_at` itself once its last phase starts, and that update is the request. Stripe can also set `cancel_at_period_end` itself after a disputed payment, if the account asks it to, and this rule sends that as a request too. `ends_at` is `new Date(cancel_at * 1000).toISOString()`, left out when `cancel_at` is null. `reason` is `cancellation_details.feedback`, left out when that is null. A subscription Stripe cancels at once sends only `customer.subscription.deleted`, which is a `churned` alone. Stripe spells it `canceled` and Barua spells it `cancelled`. A Stripe trial ends in one of two ways (https://docs.stripe.com/billing/subscriptions/trials). It leaves `trialing` in a `customer.subscription.updated` when it turns into a paid plan, or when Stripe pauses it or leaves it `past_due` because no payment method is on file. A trial that is cancelled, or that ends without a payment method on a subscription set to cancel, sends only `customer.subscription.deleted`. The two `trial_ended` rows cover both, so every trial sends one. A trial that turned into a paid plan and is cancelled later sends its `trial_ended` again from the delete with the same `occurred_at`. Barua keeps that one as a `duplicate`. Leave `plan` out of both, because a plan changed after the trial would turn that repeat into `event_id_conflict`. Stripe's `charge.refunded` fires for partial refunds too, and its charge doesn't list its refunds unless you expand them, so map each refund once, from the refund events in the table. Read the charge with `stripe.charges.retrieve(refund.charge)`. Its `refunded` is true only once the whole charge is refunded, so a refund that leaves part of the payment sends `full: false`. `occurred_at` is the refund's `created`, converted from Unix seconds, so a resend repeats the same event. Segment's `Order Refunded`, GA4's `refund` and Klaviyo's `Refunded Order` carry no refund id, so take `refund_id` and `full` from your own refund record. Stripe's `invoice.payment_failed` has no core event. When the product emails people about a failed payment, propose a custom `payment_failed` event with an `invoice_id` and an `update_payment_url` the email links to. Give it the `amount` and `currency` due as well, from the invoice's `amount_due` and `currency` through `stripeAmount`, and make both required, because a custom event has no rule that keeps the two together. A play on it is marketing email. Only Barua's own receipt and order updates, from the core order events, go out without marketing consent. Consent: send `marketing_opt_in: false` only for a recorded negative choice, such as an unchecked checkbox submitted with signup or someone turning marketing email off. Preserve an actual boolean `false` from that stored preference; a truthiness check must not drop it. Leave the property out when there is no recorded choice. Under double opt-in, send `true` when they confirm their address, never when they submit the form. ## Send events `POST /api/integrations/v1/events` with `{"records": [...]}`, one record per event. The `Idempotency-Key` header is optional for events. ```sh curl -X POST https://barua.ai/api/integrations/v1/events \ -H "Authorization: Bearer $BARUA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"records":[{"event_id":"signed_up:4812","name":"signed_up","external_id":"4812","occurred_at":"2026-09-23T10:15:00Z","properties":{"email":"ada@example.com","marketing_opt_in":true}}]}' ``` Send `signed_up` before a user's other events. Barua ties each event to its user through `external_id`, and an event whose `signed_up` hasn't arrived is held for at least 36 seconds, and up to about 10 minutes when little else is queued. Then it fails with `unknown_identity`. Resend it once the `signed_up` is in. Send history with `"mode": "backfill"`: `{"mode": "backfill", "records": [...]}`. Live is the default mode. The workspace's `deploy_live_key` step finishes when a `signed_up` sent in live mode, and received within 72 hours of its `occurred_at`, applies through an active live key. A request in backfill mode never finishes it, so sending history that way leaves the step to your product's own signups. In a backfill, send every user's `signed_up` first, and send their other events once those requests have finished applying. Each 202 has a `receipt_id`, and `GET /api/integrations/v1/receipts/{receipt_id}` shows when its events have settled. A 202 means Barua stored every event with a disposition. An invalid event is kept with its reasons and never fails the whole request. `records` has one entry per event, in the order you sent them, each with `ordinal`, `disposition`, `reason`, and for a quarantined event `reasons`, each with the `field` it names. | Disposition | What it means | | --- | --- | | `pending` | Accepted. Barua applies it to its user shortly. | | `applied` | Stored on the user. | | `duplicate` | Barua already has this `event_id` with the same content. Nothing changed. | | `quarantined` | The event failed a check. Barua keeps its reasons for seven days and applies nothing. | | `failed` | It passed every check but couldn't be applied. `reason` says why. | | `erased` | The user asked to be erased. | | `validated` | Test keys only. The event passed every check, and Barua stopped there. | ## Limits | Request | Limit | | --- | --- | | `POST /events`, `live` mode (the default) | 100 events, 256 KiB | | `POST /events`, `backfill` mode | 1,000 events, 2 MiB | | Test key events | 10,000 at once per workspace, refilling at 10,000 per 24 hours | | `POST /plan` | 256 KiB, and 50 new proposals per workspace in 24 hours | | `PUT /brand` | 32 KiB per request. The proposal Barua stores, the normalized tokens and `sources` as compact UTF-8 JSON, is at most 14 KiB, and a larger one answers 400 `brand_invalid`. | | `POST /people/erase` | 1 KiB | A 429 carries `Retry-After`. Wait that long, then send the same request again. ## Reason codes A quarantined event lists up to 10 reasons, each with a `field` path such as `properties.title`. The last six codes come from applying an event to its user, so a test key never reports them. | Code | Meaning | Fix | | --- | --- | --- | | `invalid_event` | The event is not a JSON object. | Send each event as an object with `event_id`, `name`, `external_id` and `occurred_at`. | | `missing_field` | A required envelope field is missing. | Add `event_id`, `name`, `external_id` and `occurred_at` to every event. | | `unknown_field` | The envelope has a field Barua doesn't accept. | Move event data into `properties`. | | `unknown_event` | Barua doesn't know this event name. | Use a core@1 event. To add a new one, propose it with `POST /api/integrations/v1/plan`. It works once the owner approves it. | | `invalid_event_id` | event_id must look like name:id, for example created:4812. When the event's plan entry has id_from, the id is the value id_from names. When id_from names several, the id is their values joined by colons. | Build it from the event name and the source row id, joined by a colon. An event without id_from gets one id per occurrence. | | `invalid_external_id` | `external_id` must be your user id, 1 to 256 characters. | Send the id your backend uses for this user, as a string. | | `invalid_occurred_at` | `occurred_at` must be an ISO 8601 timestamp with a timezone, after 2000. | Send the source row's timestamp, for example `2026-09-23T10:15:00Z`. | | `occurred_in_future` | `occurred_at` is more than 5 minutes after Barua received the event. | Send the time the fact became true, taken from the source row. If this repeats, check the server clock. | | `missing_property` | A required property is missing. | Add the property named in `field`. `GET /api/integrations/v1/plan` lists what each event needs. | | `invalid_property` | A property has the wrong type or format. | Send the type that `GET /api/integrations/v1/plan` names for this property. | | `unknown_property` | This event doesn't define that property. | Remove it. For a custom event, add it to the plan with `POST /api/integrations/v1/plan` first. | | `properties_too_large` | `properties` is larger than 8 KiB. | Send ids and short values. Barua needs only the fact the event records. | | `unknown_identity` | Barua doesn't know this user yet. | First send this user's `signed_up` event, or their record on `POST /api/integrations/v1/identities`. Then resend this one. Resending is safe. | | `identity_conflict` | This user's email clashes with a record Barua trusts more. | Check the email and `external_id` you send for this user. | | `event_id_conflict` | This `event_id` was already used for a different event. | Give every occurrence its own `event_id`, like `activated:`. | | `ownership_conflict` | This order already belongs to another user. | Give every order its own id. | | `version_conflict` | Barua already has a different version of this record with the same timestamp. | Send each change with a later timestamp than the one before it. | | `retry_exhausted` | Barua gave up applying this record after five attempts. | Resend it. Resending is safe. If it fails again, email chris@barua.ai. | ## Errors Every error from `/api/integrations/v1` has the body `{"code": "...", "message": "...", "fix": "..."}`. Branch on `code`: it always comes from this table, and a code is never renamed or removed. `message` describes this request; `fix` says what to do next. | Code | Status | Fix | | --- | --- | --- | | `invalid_key` | 401 | Send Authorization: Bearer ik_... with an active integration key. A workspace owner creates one with `POST /api/integrations/keys`. Keep it in your server's secret store. | | `invalid_request` | 400 | Check the request against this endpoint's section of this page. For events, `GET /api/integrations/v1/plan` returns the schema. | | `unsupported_media_type` | 415 | Send uncompressed JSON with `Content-Type: application/json`. | | `too_large` | 413 | Send a smaller request. This page lists each endpoint's limit. | | `idempotency_conflict` | 409 | Reuse an `Idempotency-Key` only with the exact same bytes. A new request needs a new key. | | `rate_limited` | 429 | Wait for `Retry-After`, then send the same request again. Keep its `Idempotency-Key` if it has one. | | `not_found` | 404 | Check the path and id. Receipts expire seven days after they complete. | | `intake_disabled` | 503 | This workspace can't send records yet. Write to chris@barua.ai to enable it. | | `unavailable` | 503 | Retry the same request with backoff. Repeating it is safe. | | `plan_invalid` | 400 | Fix the fields named in `message`, then propose the full plan again. Names are snake_case, and core event names are taken. | | `test_key_not_allowed` | 403 | Test keys can't change live data or read people. Make this call from production with the live key. | | `feed_not_enabled` | 403 | Keep using your test key. An authorized management connection can request intake.enable after setup is ready. | | `send_in_flight` | 409 | A message to this person is finishing. Retry in two minutes. | | `brand_invalid` | 400 | Fix the token named in `message` and send the whole proposal again. Send only tokens you found in the repo. | | `capability_required` | 403 | Use a key with permission for this action. A workspace owner can authorize a new key. | | `history_permission_required` | 403 | Use a key with history:write permission for backfills or events older than 72 hours. Keep retrying an accepted request with its original key and Idempotency-Key. | ## Custom events For a moment only your product has, such as a report export, propose custom events with `POST /api/integrations/v1/plan`. Send the full set every time: an approved event you leave out is removed once the new version is approved. Use `not_emitted` for each declarable core event your product has no source for. ```json { "events": [ { "name": "report_exported", "description": "Someone exported a report. Sent when the export row commits.", "properties": { "report_name": { "type": "string", "required": true, "use": "copy", "max_length": 120 } } } ], "not_emitted": [{ "name": "purchased", "reason": "We have no payments yet." }] } ``` | Field | Rule | | --- | --- | | `events` | Up to 25 custom events. `[]` removes them all. | | `name` | Matches `^[a-z][a-z0-9_]{0,63}$`, for example `report_exported`. It can't be a core event name or start with `barua_`, and `constructor` and `prototype` are reserved. The fixed names `pixel_page_viewed`, `pixel_product_viewed`, `pixel_searched` and `pixel_cart_observed` are reserved for pixel activity. It appears once per plan. | | `description` | 1 to 280 characters, trimmed, with no control characters or unpaired UTF-16 surrogates. Say when the event fires in business terms. The owner reads it before approving. | | `properties` | Up to 20. Each key matches the same pattern as `name`. `event_id`, `occurred_at`, `copy`, `constructor` and `prototype` are reserved: the first two belong to the envelope. Leave it out or send `{}` for none. `null` is refused. | | `type` | One of `string`, `number`, `integer`, `boolean`, `timestamp`, `url`, `currency`. Send email addresses only as `signed_up.email`. | | `required` | `true` or `false`. | | `use` | `copy` or `targeting`, with the same meaning as for core properties. | | `max_length` | Strings only, 1 to 500 characters. It defaults to 200. A `url` is always at most 2048. | | `not_emitted` | Up to 4 declarations, each `{name, reason}`. `name` is a declarable core event: `activated`, `created`, `purchased` or `churned`, each at most once. `signed_up` is required and can't be declared. `reason` follows the `description` rule. Leave it out or send `[]` for none. `null` is refused. | A new proposal answers 201 with a `needs_human` object, `{step, url, why, resume}`. Give the workspace owner `url` and `why`, then follow `resume`. Barua accepts a custom event only after the owner or an authorized agent approves the version that defines it. Until then it arrives as `unknown_event`, and verify lists it under `awaiting_approval`. Sending the same plan again is safe: it answers 200 with the same version. When it matches the approved plan, the answer is `"status": "approved"` with `needs_human: null`, which means nothing changed. ## Check what arrived `GET /api/integrations/v1/events/status?since=` reports, per event, what Barua received since that time, for test and live keys apart. Leave `since` out for the last 24 hours. It reaches back at most 7 days. The answer holds counts, reason codes and times, never a user's id or a property value, so a test key can read the live half too. `verdict.test` and `verdict.live` each carry `exit`: 3 when `missing` or `invalid` has an entry (or more than 10,000 events failed), 4 when a setup step is still open (`needs_human`); management access may authorize that action, and 0 when nothing is left. The skill's `scripts/verify.mjs` prints the verdict for one mode and exits with it. When more than 10,000 failed, `failures_truncated` is `true` and the exit stays 3 for any window that still holds those failures, so fix what is listed and check again with a `since` after your fix. ## Setup handoffs `GET /api/integrations/v1/setup` returns `{complete, needs_human, optional, upcoming, done}`. Each handoff entry is `{step, url, why, resume}`. For event-only access, give the owner `url` and `why`, then follow `resume`. An authorized management connection may complete these steps within its grant. The workspace owner sees the same steps on the Setup screen, worked out from the same facts. `upcoming` contains `approve_templates` or `enable_play_emails` while that step waits on preparation or another prerequisite. These links keep the next review visible if authoring finishes during an agent's final reply. They are not ready actions and grant no authority. Recheck setup before acting. Ready steps stay in `needs_human`; completed steps stay in `done`. Older servers may omit `upcoming`; the helper preserves their pending email steps using current workspace links. A supplied array, even when empty, takes precedence over that fallback. The current steps, in the order they're listed: | Step | Pending while | | --- | --- | | `create_keys` | The workspace has no active test key, or no active live key. | | `verify_domain` | The sending domain isn't verified, or the From address doesn't align with it. | | `approve_tracking_plan` | A plan proposal waits for the owner. | | `forward_email` | Optional, and listed under `optional`. Nobody has forwarded one of your emails yet. Barua reuses its design; without one, Barua uses its own. | | `confirm_brand` | The owner hasn't confirmed a brand yet. Once confirmed it stays done, and a later proposal changes nothing. | | `approve_templates` | The brand is confirmed and some play's email waits for the owner's review, or Barua couldn't write a play's emails and nothing is retrying them: the owner sees why under Plays, and an edit to the brand or playbook tries again. While Barua is still writing them, this stays out of `needs_human`. | | `enable_play_emails` | Every template is approved, but play emails aren't on. Approving templates doesn't turn them on. The owner can use Setup, or an authorized management connection can request `sending.enable`. Do this before the live deploy, because an event from before play emails are first on never starts a play. | | `enable_live_events` | Both keys exist and the test events have passed, but live events aren't switched on for the workspace. An authorized management connection can request `intake.enable` once setup is ready. The owner can grant access and review a request in Settings, Agents. | | `deploy_live_key` | Live events are on and the test events have passed, but no live `signed_up` has applied through an active live key. Live means sent in `live` mode and received within 72 hours of its `occurred_at`. A backfill never counts. While play emails are off it isn't listed, so the account that proves the deploy can also start a play. | ## Brand tokens `PUT /api/integrations/v1/brand` saves brand tokens you found in the repo as a proposal. Nothing changes until the workspace owner confirms it, so send only tokens you found, with the `file:line` each came from in `sources`. ```json { "tokens": { "from_name": "Acme", "voice": "Plain and warm. Short sentences.", "template": { "accent_color": "#e4572e", "logo_url": "https://acme.example/logo.png", "font_family": "Inter, sans-serif", "footer_name": "Acme Inc" } }, "sources": { "from_name": "web/app/layout.tsx:8", "voice": "docs/brand.md:3", "template.accent_color": "web/tailwind.config.ts:12", "template.logo_url": "web/app/layout.tsx:14", "template.font_family": "web/tailwind.config.ts:20", "template.footer_name": "web/components/Footer.tsx:5" } } ``` The tokens are `from_name`, `voice`, `language_policy` and `template` with `accent_color`, `logo_url`, `font_family` and `footer_name`. A proposal can't set `site_url`, `from_email`, `reply_to`, `send_policy`, `template.colors` or `template.html`: the owner sets those in Settings, or they come from a forwarded email. Any other key, or an empty string, answers 400 `brand_invalid`. The same tokens again answer `replayed: true` and keep the proposal waiting since the first time, with the `sources` you sent last. Once the owner has confirmed a brand, nothing is stored and the answer has `"status": "confirmed"` and `needs_human: null`. A replay with no `sources`, left out or `{}`, keeps the ones the proposal has. ## Erase a person When someone deletes their account in your product, read their email before the delete, and in the same transaction write an erase job that keeps the user's id and that email: a row in an outbox table, or a job in a queue that commits with the transaction. When the delete runs outside your database, such as through an auth provider's API, write the job just before the delete and remove it if the delete fails. That job waits until the account is gone before it calls erase, so an erase never reaches someone whose delete failed. The erase job calls `POST /api/integrations/v1/people/erase` from your server with the live key, twice: once with `external_id` and once with `email`, as `{"external_id": "4812"}` and then `{"email": "ada@example.com"}`. A request carries one of the two, never both. With `external_id`, Barua erases the address it holds for that user, which came from their `signed_up` event or identity record and is an old one when the email changed since. The job retries each call until it answers 200 with `"erased": true`, unless the table below says to stop. It's finished once both calls have answered that way, and no other answer finishes it. Calling erase again is safe: a call that already went through gets the same answer with zero counts. | Answer | What the job does | | --- | --- | | 200 with `"erased": true` | That call is done. | | 409 `send_in_flight` | An email to them is on its way, and nothing was erased. Retry in two minutes. | | 429 | Retry once `Retry-After` has passed. | | 5xx, or no answer within 30 seconds | Retry with backoff, doubling the wait up to an hour. A 503 that says the erasure ran out of time fails the same way until Barua's team finishes it by hand, so keep retrying about once an hour, and write to chris@barua.ai if it keeps failing. | | Any other answer, such as another 4xx or a 2xx without `"erased": true` | A retry fails the same way, so stop retrying and log the answer's status, with its `code` and `fix` when it has them. Keep the job, so it can run again once the cause is fixed. A test key's 403 `test_key_not_allowed` is one of these. | Log each failed attempt with the job's id and the answer's `code`, never the email or the key. Never make the erase a call inside the deletion request, awaited or under `waitUntil`. A 409 asks for two minutes and a 503 can last hours, longer than any request waits, and once the account is deleted nothing else in your product holds the id and the email. A call that fails there leaves the person's data in Barua for good. If your product has no job queue, add a small outbox table for erase jobs, written in the delete's transaction, and a scheduled job that runs the ones that are due and records each answer. ## Finish an event-only run Do this immediately before the final chat reply. Authoring and owner decisions can change setup while you write the artifact or check the repo; an earlier snapshot can omit a step that is now ready. 1. Read `GET /api/integrations/v1/plan`, then `GET /api/integrations/v1/setup` again. Derive the plan line from `plan_version` (approved) and `proposal.version` when a proposal exists; keep both versions distinct. Record the setup response with `handoffs.mjs --record `, then pass the same response through stdin to `handoffs.mjs --report `. 2. Use that output to finish the handoff block in the [Report format](https://barua.ai/skills/barua-setup/SKILL.md#report-format). Include the event sources and plan version, plus every `needs_server_call` line from repo discovery. Keep the helper's exact step identifiers and URLs, including `completed_owner_step` receipts. Refresh the artifact's handoff with this same block if its state has changed. Keep `upcoming_owner_step` lines too. They name conditional review or sending steps that may become ready while you write the reply. They are not ready in the captured response and grant no permission to act. 3. Put the full handoff block in the **final chat reply** as a text code block, followed by clickable private review-artifact and journey Markdown/JSON links. Keep this block in chat even when the linked artifact also contains it. State the verified result and remaining work briefly around the block; preserve its exact lines when shortening the reply. If the fresh setup read fails, retain the last verified block and name its age and the failed read. Do not claim the current owner steps were verified. An older server may omit `upcoming`; keep the returned steps and say that later review steps were not supplied. Do not invent their status or URLs. Before the owner makes the new account that proves the deploy, read `GET /api/integrations/v1/setup`. It lists `deploy_live_key` only once play emails are on, even while verify's live verdict names it. While it lists `enable_play_emails`, ask the owner to turn play emails on first: that account would still prove the deploy, but an event from before play emails are first on never starts a play. Once the live key is deployed, run `node /scripts/verify.mjs --live`. Your test key reads the live verdict too. It exits 4 until live events are on and the deploy is proven: the `deploy_live_key` step clears only when a `signed_up` sent in live mode applies through the live key within 72 hours of its `occurred_at`, which is what a new account made through the product sends. History sent with `"mode": "backfill"` never clears it. Exit 0 means Barua sees the product's events live. --- Source: https://barua.ai/skills/barua-setup/SKILL.md --- name: barua-setup description: Sets up and maintains Barua email for a product. Reads repos, writes server-side emitters, manages plans, plays, saved segments and one-off campaigns under an owner's permissions, and verifies fresh delivery. Use for setup, event changes, brand or play updates, campaigns, launch, key rotation, and event errors. Supports full delegation or owner review at selected checkpoints. license: MIT metadata: version: "2" --- # Barua setup Use Barua for lifecycle email and one-off campaigns. Follow the access path below, then the required journey review before selecting events or plays. Read this whole file and each required branch reference. If a tool truncates output, read the missing ranges until complete. A heading or clipped-line preview does not replace the full instructions. ## Choose your access path For the owner's own account, workspace setup or an exact approval, read [owner.md](https://barua.ai/skills/barua-setup/references/owner.md) and use `scripts/owner.mjs`. It signs in without the app and keeps credentials private. Early-access admission still applies. Read the current legal documents before supplying an exact acceptance; never accept terms automatically. Owner access can create or recover the management connection used below. - With `BARUA_MANAGEMENT_KEY`, read [management.md](https://barua.ai/skills/barua-setup/references/management.md) before any action. Run `scripts/manage.mjs capabilities` and `setup`, then follow [Complete setup and later changes](https://barua.ai/skills/barua-setup/references/management.md#complete-setup-and-later-changes). The returned grant and review policy determine what you can do. Complete [Finish and hand off](https://barua.ai/skills/barua-setup/references/management.md#finish-and-hand-off) before every final reply, including a resumed operation or partial run. - With only `BARUA_API_KEY`, follow the event-only steps below using a test key. `scripts/verify.mjs` validates events; it cannot establish provider delivery. Before creating or handing off credentials, follow [Access and discovery](https://barua.ai/skills/barua-setup/references/management.md#access-and-discovery). The management key (`mk_`) has no test or production mode. Event keys (`ik_`) have recorded test and live modes. For a missing management key, follow [Recover a lost management key](https://barua.ai/skills/barua-setup/references/management.md#recover-a-lost-management-key); preserve the existing connection. For both paths, read and complete [journey-review.md](https://barua.ai/skills/barua-setup/references/journey-review.md): native source discovery before drafting, then a separate fresh independent review before any onboarding plan proposal, approval or brand submission. Retain the complete journey artifacts and reconcile exact Play revisions as that procedure requires. Full delegation adds no human pause. Produce the current [review package](https://barua.ai/skills/barua-setup/references/review-package.md) for the handoff, including incomplete runs. Use plain factual prose; name the verified outcome and next action. Avoid promotional filler, dramatic fragments, repetitive lists and contrast slogans. ## Saved segments and one-off campaigns Before segment or campaign actions, read and follow [Send one campaign to a saved segment](https://barua.ai/skills/barua-setup/references/management.md#send-one-campaign-to-a-saved-segment). Setup and Play permissions do not grant campaign sending. The event API is `https://barua.ai/api/integrations/v1`, using `Authorization: Bearer $BARUA_API_KEY`. `BARUA_API_URL` overrides the origin. HTTPS is required except on localhost. Read `references/events.md` for the contract and `references/reading-repos.md` for repo discovery. ## Install ```sh npx skills add getbarua/skills --skill barua-setup ``` Without flags it asks which agents to install for. For one agent without prompts, add `-a claude-code` or `-a codex` and `-y`: ```sh npx skills add getbarua/skills --skill barua-setup -a claude-code -y ``` Or copy this folder to `.claude/skills/barua-setup/` for Claude Code, or to `.agents/skills/barua-setup/` for Codex. Install the whole folder: `review-package` needs `scripts/review-package.mjs` beside `scripts/manage.mjs`. Owner access needs `scripts/owner-storage.mjs` beside `scripts/owner.mjs`. Both owner and management helpers need `scripts/private-input.mjs` beside them. For headless customer, segment, email and diagnostic reads, use [inspection.md](https://barua.ai/skills/barua-setup/references/inspection.md). `scripts/inspect.mjs` needs `scripts/inspect-output.mjs`, `scripts/owner-storage.mjs` and `scripts/private-input.mjs` beside it. It saves email previews privately without loading tracking or unsubscribe links. The task formatters need [`templates/journey-discovery-task.md`](https://barua.ai/skills/barua-setup/templates/journey-discovery-task.md) and [`templates/journey-review-task.md`](https://barua.ai/skills/barua-setup/templates/journey-review-task.md); final review reads [`references/events.md`](https://barua.ai/skills/barua-setup/references/events.md) after its own source-first checklist. When downloading helpers manually, preserve all three relative paths beside the `scripts` directory. ## Before you start This section and the numbered steps below cover event-only access. Use the management reference for delegated actions. - The test key goes in a dev env file as `BARUA_API_KEY`, with an optional `BARUA_API_URL`. verify takes each from the environment first, then from `.env.local`, then from `.env`, in the directory you run it from. Run it from the folder that holds the file. - Before you write the key into a file, run `git check-ignore -q ` on the exact file, such as `.env.local`. It exits 0 only when git ignores the file and doesn't track it. On any other exit, stop: don't write the key there, and ask the owner where the product keeps its dev secrets. Stop the same way when `test -L ` exits 0, because the file is a symlink and the key would land wherever it leads. - Every call to Barua and every flow you trigger uses the test key. The live key goes only into production's hosting secrets, where the emitters and the erase job use it. Erase takes only the live key, so the test key gets 403 `test_key_not_allowed`, which is expected. If verify exits 2, stop and ask the workspace owner for a Barua test key. Never ask for the live key. - Create one private handoff receipt file for this run with `mktemp`. Keep its path. Pass each plan or setup JSON response to `node /scripts/handoffs.mjs --record ` through stdin when you receive it. The helper saves owner steps and their links, plus local server gaps recorded during repo inspection. Show its handoff text to the owner. An approval can finish before the next status read, so keep the receipt through the final report. ## Steps Every step is safe to run again on a repo an earlier run set up. Change what that run wrote in place rather than adding a second copy: the emitter with its emit calls, and the `AGENTS.md` block with its `@AGENTS.md` line. Step 4 sends the full plan again, which Barua matches against the approved one, and step 7 leaves a confirmed brand alone. 1. Check the key before any other call to Barua: run `node /scripts/verify.mjs` with no flags, where `` is this skill's folder. It only reads. If it exits 2, stop: there is no test key to use, and every later step would act with a live key. On exit 1, fix what `error` says and run it again. Any other exit means a test key. If the key changes during the run, start again here. 2. Read the contract: `GET /api/integrations/v1/plan`. It lists the core events (`signed_up` is required), any approved custom events, each event's `id_from`, and a JSON Schema for one event. 3. Read every repo the owner named, as `references/reading-repos.md` describes. Complete and review the journey artifacts required by `references/journey-review.md` before step 4. For the supporting core and custom facts, find where each becomes true and record missing instrumentation as work to implement. Record frontend actions without a server request using the handoff helper's local receipt format, including their source and suggested endpoint. 4. Propose the plan with `POST /api/integrations/v1/plan` on every run, reruns included. Send the full plan: every custom event the product needs, and a `not_emitted` entry with a reason for each declarable core event the product has no source for (`GET /plan` marks those `declarable`). An approved event or declaration you leave out is removed, so send `"events": []` when the product needs none. The optional `consent_updated` stays out of the plan when the product has no source for it, and goes in the report instead. Always propose even when both arrays are empty: a core-only product still needs an approved plan. Barua compares the plan with the approved one, and `"status": "approved"` means nothing changed. Otherwise create the readable event-plan artifact from `references/review-package.md` and show it with the answer's `needs_human` `url` and `why`, then keep building while the owner approves. 5. Write the emitter. Copy `references/emit.ts` into the backend (as `emit.mjs` with the types removed, for plain JavaScript), or write the plain HTTP call from `references/events.md` for another stack. Add an emit call at each commit point. If the product deletes accounts, read the user's email before the delete, and in the same transaction write an erase job that keeps the user's id and that email. The job calls `POST /api/integrations/v1/people/erase` twice, with the id as `external_id` and then with the `email`, because the address Barua holds for the id can be an old one when the email changed. It retries each call until it answers 200 with `"erased": true`, the way `references/events.md` says under "Erase a person", which also covers a delete your database doesn't run and a product with no job queue. 6. Check it. Step 1 found a test key, so note the current time (`date -u +%Y-%m-%dT%H:%M:%SZ`) and trigger each flow through the product's real code, such as its test suite or a local run. Then run `node /scripts/verify.mjs --since `. Fix what it lists and run it again, as the exit codes below say, until it exits 0 or 4. verify checks the key on every run, so a key that turns live ends the loop at exit 2 before the next flows. For a queue or outbox, also hold delivery at a local test endpoint while exercising a product request: the request must complete with its event saved before delivery resumes. Check a concurrent request too, to catch a worker holding the product's database lock during HTTP. 7. Propose brand tokens with `PUT /api/integrations/v1/brand`: the tokens you found, and in `sources` the `file:line` each came from. `references/events.md` lists the tokens it accepts. When you found no token, as in a backend-only repo, skip the call: Barua refuses an empty proposal, and a token with no source is made up. `GET /setup` then keeps `confirm_brand` open until the owner confirms a brand. Before you send it, read `GET /api/integrations/v1/setup`, and skip the call when its `done` list holds `confirm_brand`. That means the owner confirmed a brand, and a proposal sent after that changes nothing: the owner edits a confirmed brand in Settings. While `confirm_brand` is open, send it: the same tokens again keep the proposal the owner has and take the `sources` you send now (sending none keeps the ones it has), so a token found on another line shows its new place, and new tokens replace the proposal. 8. Read `GET /api/integrations/v1/setup` and record its response with the handoff helper. Pass the same response through stdin to `node /scripts/handoffs.mjs --report `. Show that report to the owner: it retains local server gaps and earlier review links, and marks completed owner steps from the current `done` list. Preserve its exact lines in your final report, including completed steps. 9. Add `templates/AGENTS.md` to the root `AGENTS.md` of the repo that holds the emitters, and create that file if it's missing. Codex reads it. Claude Code reads `AGENTS.md` on its own only when the repo has no `CLAUDE.md`, so make sure the root `CLAUDE.md` imports it with `@AGENTS.md` on a line of its own: add the line to an existing `CLAUDE.md` that doesn't have it, or create `CLAUDE.md` with only that line. Codex reads an `AGENTS.override.md` instead of the `AGENTS.md` beside it, so when the root has one, add the block there too. Replace an existing block between the same markers. Fill in each placeholder with a path from the repo root: `` with the emitter's, such as `src/lib/barua.ts`, and `` with this skill's folder, such as `.claude/skills/barua-setup`. When the skill sits outside the repo, install it into the repo first, as under Install, so a later agent finds verify at that path. Before you create or change any of these files, run `test -L `, which exits 0 when the file is a symlink, a broken one included. A `CLAUDE.md` or `AGENTS.override.md` that links to the root `AGENTS.md` already shares it, so leave it as it is. Write nothing to or through any other link, and when `AGENTS.md` itself is a link, change none of the three files. Report each link you left as a `needs_human: linked_file` line. 10. Refresh the readable event-plan artifact from `references/review-package.md`. Complete [Finish an event-only run](https://barua.ai/skills/barua-setup/SKILL.md#finish-an-event-only-run) after all artifact writing, tests and repo checks. ## Finish an event-only run Immediately before the final chat reply, follow [Finish an event-only run](https://barua.ai/skills/barua-setup/references/events.md#finish-an-event-only-run). Reread plan and setup after writing artifacts and checking the repo. Preserve the helper’s exact handoff block, including completed and conditional upcoming steps, in both chat and the private artifact. Follow that reference for the later live-deploy check: a `signed_up` must apply through the live key in live mode within 72 hours of its `occurred_at`; backfills never count. ## When verify exits 4 For event-only access, show the owner each `needs_human` entry's `url` and `why`, and finish any other work. Then run verify again every 30 seconds for up to 10 minutes. If it still exits 4, report the open steps. With management access, read management setup and use the allowed action or resume the exact approval operation. The legacy `needs_human` label does not override a delegated grant. `awaiting_approval` lists events the pending plan adds or stops requiring. They clear when the owner approves the plan. ## ALWAYS - Send events from server code after the database commit, from a job queue or the platform's `waitUntil`, else `await` the send before responding. That last fallback applies only when there is no durable queue or outbox, including one you add. With a queue or outbox, the request waits only for the transaction that saves the fact and event; it never waits for the worker, flush, drain or delivery. The worker awaits and retries HTTP outside the database transaction. A `consent_updated` that turns marketing off goes only through a job queue or an outbox, because a lost one leaves Barua treating someone who said no as opted in. `references/reading-repos.md` has the order, and how to fit `attempts` to the time the send gets. - Check `ok` on every emit result, and log a failure with the event's name and `event_id`, never the key. `emit` never throws, so a failure nobody logs is an event lost without a trace. - Build `event_id` as `:`, one per occurrence for repeatable events. When `GET /plan` gives an event an `id_from`, the id is the value it names (`purchased` uses `properties.order_id`). For `checkout_started` it names two values, so the id is `checkout_started::`, and `start_id` can't contain a colon. A few events build the last part from a value, such as `signed_in`, whose id ends with the UTC date. `references/events.md` lists them. - Take `occurred_at` from the source row. - Send `purchased` only once the payment provider confirms the money arrived. A processing payment or a refund never sends it. `references/reading-repos.md` has the rule for Stripe and other providers. - Use the test event key for development and CI event calls. Use the management key only with the management API under its grant; it always controls the real workspace. - Trust verify's exit code over your own reading. - Declare each declarable core event the product has no source for as `not_emitted` in the plan, with a reason. A missing emitter for an existing product fact is implementation work, not a reason to declare it absent. `signed_up` can't be declared, and neither can the optional `consent_updated`, `checkout_started` or any other optional core event, which go in the report instead. Never fake an event to fill the gap. - Send history with `"mode": "backfill"` (`emit(events, { mode: 'backfill' })`). Only events the product sends as they happen prove the live deploy. - Report every emit site as `file:function`, every `needs_server_call`, every core event the product has no source for and the moment you chose for `activated`. - Give the owner a readable artifact with the event rationale and source evidence. Include proposed play rules and exact email previews when management access permits reading them. Preserve the current review policy; an exported report is a dated snapshot, never an approval or delivery receipt. ## NEVER - Send server facts from browser code, including the pixel's `track()`, or from a database trigger. The pixel's fixed browsing observations use their own ingestion path; never send their reserved names through the server events API or count them as server delivery proof. Never make a job that reads product rows and builds events from them an event's only source. An outbox is different: the commit writes the finished event, and its worker only sends it. A scheduled reconciler may re-send events the emit site already sent, built by the same code with the same `event_id` and `occurred_at`, so a resend is a `duplicate` and never a new fact. - Start a send that nothing waits for (`void emit(...)`). A serverless or edge platform can end the invocation after the response, and the event is lost along with its failure log. - Erase from the deletion request alone, awaited or under `waitUntil`. Once the account is gone, only the erase job still holds the id and the email (step 5). - With event-only access, ask for or read a live key. Do not approve an action or claim approval through that access path. With management access, act only within the current grant and report the recorded executor accurately. - Print any key or write it into a tracked file. - Send hand-built events, or add an emit for a fact the product doesn't have, to make verify pass. - Send source code, env values or a repo map to Barua. Send the declared events and the specific configuration fields the API requires, including a repo-derived playbook when using management access. - Run a history import without the separate `history:write` grant and authority to read that source. Never use backfill to pass live proof. - Write to a file through a symlink, such as the env file or `AGENTS.md`, because a link can lead out of the repo to a file other projects share. Run `test -L ` before you create or change a file. Step 9 says which links can stay. Leave any other link as it is and report it as a `needs_human: linked_file` line. ## Exit codes verify prints one JSON object: `ok`, `exit`, `mode`, `since`, `key_mode`, `plan_version`, `missing`, `invalid`, `awaiting_approval`, `not_seen_yet`, `not_emitted`, `needs_human`, `warnings` and `error`. Each `invalid` and `missing` entry carries its own `fix`. | Exit | Meaning | What to do | | --- | --- | --- | | 0 | Nothing left to do for this mode. | Report. | | 1 | verify couldn't run. | Read `error`, fix the flag or the network, and run it again. | | 2 | No test key to use, such as a live key, or Barua refused the key. | Stop. Make no more calls to Barua and run no more flows. Ask the workspace owner for a test key. | | 3 | The agent can fix the events. | Fix each entry in `missing` and `invalid`. Then trigger the flows and run verify again with the same `--since`. When `warnings` says more than 10,000 events failed, use a `--since` after your fix instead: the failures before it keep the exit at 3 until they leave the window, even with nothing listed. | | 4 | A setup step is still open. | Follow "When verify exits 4" above; management access may authorize its action. | `not_seen_yet` (live mode only) is for information: live traffic can't be forced, and the test key already proved each emitter. ## Report format End with this report, even when verify exits 0. One line per core event and per custom event, then the erase and plan lines (and the brand line when step 7 sent nothing), then each frontend action with no server request, then each step a person still has: ```text signed_up → src/auth/signup.ts:createUser activated → src/projects/publish.ts:publishProject (first published project) created → src/projects/create.ts:createProject purchased → src/billing/webhook.ts:handleInvoicePaid churned → not_emitted: accounts never end, people stop coming back erase → src/account/delete.ts:deleteAccount queues src/jobs/baruaErase.ts:eraseFromBarua (external_id and email) plan: proposed version 2 needs_server_call: web/editor/Publish.tsx:onPublish only changes local state. Suggested endpoint: POST /api/posts/:id/publish needs_human: approve_tracking_plan https://barua.ai/app/#/w//settings/plan needs_human: confirm_brand https://barua.ai/app/#/w//settings/setup ``` - `name → file:function` for each emit site. For `activated`, add the moment you chose in parentheses, so the owner can change it. - `name → not_emitted: ` for each declarable core event the plan declares. - `consent_updated → no source: ` when the product has no way to change marketing consent later. `consent_updated` is optional, so the plan can't declare it. - `checkout_started → no source: ` when the server never creates a checkout, such as a product with no checkout or one whose checkout lives entirely in a hosted store. `checkout_started` is optional too. - ` → no source: ` for any other optional core event whose moment the product has but its server never records, such as shipments tracked only in a carrier's own tool. Leave out the ones the product has no such moment for. - `erase → file:function queues file:function (external_id and email)` when the product deletes accounts: first the account deletion that writes the erase job, then the job that calls `POST /people/erase` with `external_id` and then `email` until each answers 200 with `"erased": true`. - `plan: version ` from the plan answer. A proposal returning `approved` means it matched the approved plan and nothing changed. Name the actual executor when your management operation approves a new version. `proposed` means that version still needs approval. - `brand: no tokens found in ` when step 7 sent nothing, so the owner knows the brand is theirs to set under `confirm_brand`. - `brand: confirmed by the owner, not proposed again` when step 7 skipped the call because `confirm_brand` was done. Add the tokens you found to the line, so the owner can compare them with Settings. - `needs_server_call: . Suggested endpoint: ` for each frontend action with no server request. - `needs_human: ` for each open step from verify and `GET /setup`. Copy these literal lines into the final report's text block, even when you also give friendly Markdown links. Keep `needs_server_call` lines in that block too; ordinary prose can lose the exact action and source location. - `completed_owner_step: ` lines from the handoff helper, so an approval completed during setup still has a review link in the report. Use the helper's saved receipts and latest setup response to produce this block; a final status snapshot alone loses earlier approvals. - `upcoming_owner_step: ` lines from the current setup response, with the helper's reason and state. These are conditional next steps; recheck setup before acting. The helper prints them without saving them as observed owner requests. - `pending_owner_step: ` lines preserve future email work from older servers without `upcoming`. They are not permission to act or proof of enabled sending. If the helper needs a current workspace link, refresh setup every 30 seconds for up to 10 minutes and rerun its report when review is ready. If it still cannot produce the report, say email setup remains pending; an empty action list does not establish completion. - `needs_human: linked_file → ` for each file you left alone because it's a symlink (`readlink ` shows the target), with what you would have written there, so the owner can add it by hand. Include `review_artifact: ` in the handoff and implementation PR. For management access, link `index.html` and its `review.json` snapshot; for event-only access, link the event-plan Markdown. State any unreadable sections or failed collection. Keep the files private and do not commit or publicly host them by default. For management access, also report the connection label, completed operation IDs, any exact operation awaiting review, the deployed key's ID (never its secret), and delivery proof ID/status. Distinguish emitter validation, deployed traffic, provider acceptance and confirmed delivery. Describe external work that lacks an authorized tool without calling the entire setup blocked. --- Source: https://barua.ai/skills/barua-setup/references/inspection.md # Read Barua without the app Use `manage.mjs` for customer profiles, segments, campaigns and operation status. Use `inspect.mjs` for setup details, diagnostics, Play previews and email content. Install the whole skill: inspection needs `scripts/inspect-output.mjs`, `scripts/owner-storage.mjs` and `scripts/private-input.mjs` beside `scripts/inspect.mjs`. All helpers use Node built-ins only. Set `BARUA_MANAGEMENT_KEY` and the trusted `BARUA_URL` in the environment. The default URL is `https://barua.ai`. Owner tokens and event keys cannot substitute for a management key. Never pass credentials in arguments. Run each helper with `--help` for flags. Run `manage.mjs capabilities` first: its `reads` map names each helper, method, path and permission rule. `permissions` requires every listed scope; `any_permissions` requires at least one when nonempty. `allowed` describes those scopes, while `connection.terms_current` separately describes terms acceptance. An empty scope list still requires an authenticated, active connection. A real-person `play-preview` also requires `people:read`, represented by `conditional_permissions` for the private `personId` body field. Example previews use made-up people. Campaign detail includes customer examples only with `people:read`. Existing connections never gain new scopes automatically. All paths below are under `/api/management/v1`. These commands read or simulate; they do not save decisions or send email. `proof` here means its plain GET form, without the separate event-binding flags. Use the action workflow in [management.md](https://barua.ai/skills/barua-setup/references/management.md) for changes. | Command | Helper | Method and path | Required scopes | | --- | --- | --- | --- | | `capabilities` | `manage.mjs` | `GET /capabilities` | Connection | | `setup` | `manage.mjs` | `GET /setup` | Connection | | `usage` | `manage.mjs` | `GET /usage` | Connection | | `operations` | `manage.mjs` | `GET /operations` | Connection | | `status` | `manage.mjs` | `GET /operations/:id` | Connection | | `proof` | `manage.mjs` | `GET /proofs/:id` | Connection | | `segments` | `manage.mjs` | `GET /segments` | Connection; one of segment:write, campaign:write, campaign:send | | `segment` | `manage.mjs` | `GET /segments/:id` | Connection; one of segment:write, campaign:write, campaign:send | | `segment-fields` | `manage.mjs` | `GET /segment-fields` | Connection; one of segment:write, campaign:write, campaign:send | | `campaigns` | `manage.mjs` | `GET /campaigns` | Connection; one of segment:write, campaign:write, campaign:send | | `campaign` | `manage.mjs` | `GET /campaigns/:id` | Connection; one of segment:write, campaign:write, campaign:send | | `campaign-recipients` | `manage.mjs` | `GET /campaigns/:id/recipients` | people:read; one of segment:write, campaign:write, campaign:send | | `campaign-job` | `manage.mjs` | `GET /campaign-jobs/:id` | Connection; one of segment:write, campaign:write, campaign:send | | `play-catalog` | `manage.mjs` | `GET /plays/catalog` | Connection | | `play-job` | `manage.mjs` | `GET /plays/jobs/:jobId` | Connection | | `play` | `manage.mjs` | `GET /plays/:id/detail` | Connection | | `play-definition` | `manage.mjs` | `GET /plays/:id/definition` | Connection | | `play-validate` | `manage.mjs` | `POST /plays/:id/validate` | Connection | | `play-simulate` | `manage.mjs` | `POST /plays/:id/simulate` | Connection | | `play-runs` | `manage.mjs` | `GET /plays/:id/runs` | people:read | | `play-trace` | `manage.mjs` | `GET /plays/:id/runs/:runId` | people:read | | `play-admissions` | `manage.mjs` | `GET /plays/:id/admissions` | people:read | | `play-feedback` | `manage.mjs` | `GET /plays/:id/feedback` | Connection | | `play-paths` | `manage.mjs` | `GET /plays/:id/path-counts` | Connection | | `people` | `manage.mjs` | `GET /people` | people:read | | `people-search` | `manage.mjs` | `POST /people/search` | people:read | | `person` | `manage.mjs` | `GET /people/:id` | people:read | | `person-activity` | `manage.mjs` | `GET /people/:id/activity` | people:read | | `person-events` | `manage.mjs` | `GET /people/:id/sessions/:sessionId/events` | people:read | | `keys` | `inspect.mjs` | `GET /keys` | Connection | | `brand` | `inspect.mjs` | `GET /brand` | Connection | | `plan` | `inspect.mjs` | `GET /plan` | Connection | | `event-status` | `inspect.mjs` | `GET /events/status` | Connection | | `events` | `inspect.mjs` | `GET /diagnostics/events` | Connection | | `stats` | `inspect.mjs` | `GET /diagnostics/stats` | people:read | | `catalog` | `inspect.mjs` | `GET /plays/catalog` | Connection | | `domain` | `inspect.mjs` | `GET /domain` | Connection | | `inbound-templates` | `inspect.mjs` | `GET /templates/inbound` | setup:write | | `inbound-template` | `inspect.mjs` | `GET /templates/inbound/:id` | setup:write | | `plays` | `inspect.mjs` | `GET /plays` | Connection | | `play-map` | `inspect.mjs` | `GET /plays/map` | Connection | | `play-mode` | `inspect.mjs` | `GET /plays/mode` | Connection | | `play-results` | `inspect.mjs` | `GET /plays/results` | Connection | | `play-template` | `inspect.mjs` | `GET /plays/:id/template` | Connection | | `play-profiles` | `inspect.mjs` | `POST /plays/profiles` | people:read | | `play-preview` | `inspect.mjs` | `POST /plays/:id/preview` | Connection | | `profile-preview` | `inspect.mjs` | `POST /people/:id/preview` | people:read | | `emails` | `inspect.mjs` | `GET /emails` | people:read + emails:read | | `email` | `inspect.mjs` | `GET /emails/:id` | people:read + emails:read | | `email-preview` | `inspect.mjs` | `GET /emails/:id/preview` | people:read + emails:read | | `email-raw` | `inspect.mjs` | `GET /emails/:id/raw` | people:read + emails:read | ## Private inputs and files Supply customer searches and previews as JSON stdin or `--input-file`. `people-search` accepts exactly one of `email` and `external_id`. `play-profiles` accepts `{email, after?}`. `profile-preview --id ` accepts `{play_id?}`. `play-preview --play-id ` accepts `{revisionId, stepId, language, personId}` or `{revisionId, stepId, language, example:true}`. Never put email addresses in query strings or command flags. For stored mail, first read `inspect.mjs email --id `. It distinguishes accepted provider content, prepared content and legacy saved drafts. Legacy drafts have an unknown send-time recipient and are not exact submissions. An acceptance records what the provider accepted, not inbox placement. Save `email-preview --id --output-file ` to inspect inert text without tracking images, live links or unsubscribe requests. `email-raw` uses the same flags but saves original untrusted HTML. Do not open that raw file as a preview. Neither command prints HTML or opens a browser. Ordinary inspection output also hides HTML fields. Add `--output-file` to save the full private JSON. Files are created exclusively with mode 0600, refuse final symlinks and must already be ignored when inside Git. Existing files are never overwritten. Failed reads remove the unfinished destination. Do not commit or publish customer artifacts. Follow the read's declared pagination field, repeating the same filters. Email history returns `next`; pass it to `inspect.mjs emails --cursor` until it is null. People and activity reads return `next_cursor` for `--cursor`. For Play versions, pass the returned `nextVersion` to `manage.mjs play --play-id --after-version `, preserving any `--revision-id`. `inspect.mjs` never retries or automatically walks pages. It accepts `--timeout-ms` from 1 to 300000, defaults to 60000, and shares that time across input, HTTP and file handling. Responses have an 8 MiB cap. Statistics preserve the app's counts and optional `--health` ledger. Failed or oversized reads return unavailable instead of partial totals. Profile sections marked unavailable are not empty profiles. Customer preview uses the real eligibility reader, with 50 seconds for work and five seconds for abort and response handling. It makes no model call and sends nothing. --- Source: https://barua.ai/skills/barua-setup/references/journey-review.md # Review the customer journey before choosing events Use this procedure for initial setup and changes to events, playbooks or plays. It applies to management access and event-only access. Read access and current state first; complete this review before proposing or approving a plan. Source discovery and a fresh independent agent review are required before onboarding plan or brand submissions. Management Play approvals also require a compact reconciliation record bound to the exact policy and emails. That record cannot establish semantic truth or complete source coverage. Full delegation continues within its grant, without a new human checkpoint. An owner checkpoint presents the artifacts and waits on the existing operation. Reading or exporting configuration does not approve it. Initial setup proposes the complete intended strategy. A message the owner names is a requirement to include, unless they explicitly limit the scope. For example, a requested welcome email does not exclude recovery or returning customer opportunities. Record any actual owner boundary with its source. Choose exclusions for a stated product or priority reason; absence from the owner's examples is not a reason. ## Discover the experience Before writing the strategy, generate `--render discovery-task` using the [task context below](https://barua.ai/skills/barua-setup/references/journey-review.md#prepare-the-review-packet-and-task) and start its fresh native agent. Pass its prompt unchanged, with the specified model, effort and foreground controls. It reads product source only and returns a completed checklist. Retain the completed native result. A separate private copy is optional; do not transcribe the whole result just to create another file. It is advice, not approval, and must not be used as the final reviewer's initial context. Reuse its observed facts as the first-pass source inventory. Verify uncertain or missing evidence against the actual product, then map each assessment obligation into the strategy. Avoid writing a second independent inventory from scratch. Inventory source-owned entities and variants before selecting messages. Follow state transitions through failure and recovery. Record time conditions and existing senders, including the facts that stop their work. Keep source observations separate from the later opportunity list. Existing analytics and emitters help locate evidence; they do not determine which situations matter. Discovery rows separate `current` source behavior, the `expected` customer outcome or assessment obligation, actual action `prerequisites`, and `evidence`. Follow a proposed destination through its access and readiness checks during discovery. A fact that work finished does not prove the customer can use it. Keep an unknown product choice explicit. Existing sender silence does not resolve whether an outcome should be covered, excluded or held. Use these areas as a discovery checklist, not a requirement to invent features: - Account access, setup and first useful result, including distinct creation modes such as creating from scratch or importing existing work. - Unfinished actions, background work, failures and recovery. Distinguish accepted work, actual readiness and the customer using the result. - Checkout, confirmed payment, entitlement and fulfilment. Include paid changes, cancellation and refunds when the product supports them. - Returning customers and relevant legacy accounts. Read the current product strategy before proposing trials, subscriptions or win-back messages. - Existing service messages, delivery problems, marketing preferences, account or object deletion, and loss of access. Record the routes or state transitions that establish each situation, including the unhappy paths. Check the existing email code and provider configuration when authorized. An absent emitter is implementation work. An absent business fact may justify an exclusion or a core-event declaration; these are different. Do not declare an event `not_emitted` just because its emitter is missing. Record completion of this first pass in `discovery_review`. An uninspected source area leaves discovery incomplete; a documented unknown stays visible. ## Write the private artifact Choose an authorized private directory; use the owner's requested location when given. Keep `journey-review.json` and `journey-review.md` together there. Start from `templates/journey-review.json` in the installed skill, also available as the [public JSON template](https://barua.ai/skills/barua-setup/templates/journey-review.json). Fill the JSON as the inventory, then generate its readable view with the installed helper: ```sh umask 077 node .agents/skills/barua-setup/scripts/journey-review.mjs --render markdown \ --journey-review-file /private/authorized/journey-review.json \ > /private/authorized/journey-review.md ``` Use your installed skill path and an authorized private directory. The helper writes only to stdout; you explicitly save the output. The Markdown filename replaces the JSON suffix with `.md` in the same directory. It starts with the recorded decisions, then shows their rules and review records before the detailed source inventory. Every recorded field remains visible, including unfinished reviews, held decisions and additive metadata. Rendering does not certify discovery or approval. Change the JSON and regenerate; do not edit the generated Markdown separately. Its exact bytes use LF line endings, no BOM and one final newline. Each file must fit within 512 KiB and contain no credentials. Keep the documented fields and types; additive descriptive metadata is allowed. Keep the files private and out of commits by default; use a private directory and file permissions. Include no credentials, customer records or private message contents. Source references and product facts are sufficient. Write consumer rules as concise conditions. Keep supporting explanation in the evidence fields. Each covered consumer needs its material conditions in the fields that reach the playbook; follow the held-summary rules below for other decisions. Render and measure the playbook while drafting, before independent review; use the command and existing limits below. Do not overwrite earlier evidence from an onboarding test. For a fresh run, record the authorized empty starting state. Existing configuration makes this a resumed run; it does not authorize resetting the workspace. The inventory uses this format. Dates are ISO 8601 UTC. IDs are stable within the review and must remain unchanged when a row is refined. | Top-level field | Content | | --- | --- | | `version` | `1` | | `authoring_mode` | New artifacts use `"per_play"`: shared product facts and complete individual Play requests. Omit only when preserving a legacy artifact's full-playbook behavior. | | `product` | The product being reviewed | | `captured_at` | When the source and workspace observations were made | | `sources` | Objects with `repo` and `revision`; use the inspected commit, or explain an unversioned source | | `owner_scope` | Optional object or null. Its `requirement` is text or null, and `boundaries` is a string array from the owner's actual request; leave unknown scope unrecorded | | `source_inventory` | Source facts recorded before choosing messages | | `discovery_review` | First-pass `status`, `checked_at` and `findings`, using the same types as `coverage_review` | | `journeys` | The discovered situations, including ones that will not get an email | | `opportunities` | Explicit message or control decisions for those situations | | `coverage_review` | `status` (`unreviewed`, `complete` or `incomplete`), `checked_at`, and `findings` | | `independent_review` | The separate reviewer's recorded result and input bindings; null until available | | `workspace` | `id` and `plan_version` when readable; otherwise null | | `reconciliation` | The relationship between selected opportunities and exact current play revisions | | `submission_reviews` | The source-based claim review for each exact plan or playbook input | | `approval_observations` | The canonical execution target and reviewed previews for a management Play approval | Each `source_inventory` row has `id`, `kind`, `statement`, `status`, `evidence`, `material` (boolean) and `opportunity_ids`. IDs are distinct strings of at most 128 characters. `kind` is `entity`, `variant`, `transition`, `time_condition` or `sender`. `status` distinguishes `observed`, `proposed` and `unknown`. `evidence` and `opportunity_ids` are arrays of strings. An observed fact needs a source reference. During the second pass, link every material row to an assessed opportunity, including exclusions or controls. Do not manufacture a source fact to fill an empty category. Each `journeys` row has `id`, `situation`, `evidence` (source references), `opportunity_ids`, `assessment` (`assessed` or `unassessed`) and `notes`. A discovered situation with no desired email still needs an explicit exclusion or control decision. A list containing only the selected plays is not an inventory. Check that every opportunity reference resolves and each journey-to-opportunity link is recorded in both directions. Each `opportunities` row contains: | Field | Meaning | | --- | --- | | `id`, `journey_ids` | Stable opportunity ID and related inventory IDs | | `kind` | `lifecycle`, `service`, `micro` or `control`; this describes the customer need, not Barua's sending class | | `outcome`, `evidence` | The useful customer outcome and source facts supporting it | | `trigger` | The authoritative fact or state that starts consideration | | `actor`, `subject` | The intended recipient and exact object or occurrence, such as the same project, order or job | | `eligibility` | Current-state conditions required at entry and again before sending, including relevant facts from before the trigger | | `timing` | Proposed timing and its anchor; distinguish a proposal from an established product promise | | `stops`, `reentry` | Completion, recovery, deletion and other stopping facts; when another occurrence may qualify | | `context`, `facts` | Required wording/targeting context and supporting product facts, including facts that lack events | | `current_sender`, `proposed_sender`, `duplicate_prevention` | Current owner, proposed owner and how the same purpose/occurrence avoids duplicate messages | | `decision`, `reason` | `covered`, `excluded` or `blocked`, with the reason | | `implementation`, `dependency`, `next_action` | What actually exists, what is missing and the concrete next step | | `planned_events` | Intended event names; use core events when their meaning fits | `evidence`, `journey_ids`, `stops`, `context`, `facts` and `planned_events` are arrays of strings. `implementation` must be nonempty text on every row, including blocked and excluded rows. State what exists or that no implementation is planned; do not use null. Other opportunity fields are strings, except an unresolved rule may be null on a blocked or excluded row. Explain that absence in `dependency` and `next_action`, or the exclusion reason. Distinguish observed product facts from proposed message rules. Under delegated setup, propose concrete timing, eligibility and stops from those facts and label the choices as proposals. A new message does not itself require a new owner checkpoint. Hold an affected action when an essential fact, permission or capability is missing; do not invent those facts to fill the row. When an email needs a fixed product or action URL, put the source-reviewed absolute URL in `context` or `facts`. Preserve the known URL exactly. Read the workspace's setup and brand responses too: a recorded `site_origin` or confirmed brand site URL supplies the public origin for a source-verified product route. Record the response as evidence, combine that origin with the actual action route and verify its prerequisites. Do not call the origin unknown merely because the product source omits it. A relative route alone does not establish its origin. These fields reach the playbook and its link checks. Record the useful customer outcome and its source-supported behavior before choosing a disposition. Keep the known trigger, eligibility, timing, stops, action prerequisites and ownership. A missing capability leaves a dependency or unresolved decision; it does not erase known conditions. Existing sender behavior describes the current system, not whether a new outcome is useful. Then choose the disposition. For blocked work, describe what implementing each named dependency would make possible and what would remain unresolved. A safe hold does not establish complete strategy coverage. An exclusion needs an intentional product or scope reason. Use the decisions consistently: - **Covered:** an explicit message/control decision has concrete rules and an owner. This means strategy coverage, not that its event or play is deployed. State unfinished implementation honestly. - **Excluded:** a deliberate product or priority decision, with its reason. “There is no emitter” or “the owner did not name it” is not a reason to erase a relevant situation. Do not invent a launch phase to justify exclusions. - **Blocked:** the opportunity remains in scope, with a missing decision, capability or implementation dependency and a next action. Externally owned messages can be covered. Keep their supporting events in the intended plan when other targeting, stopping or reporting needs them. Do not assign Barua a second shipment or receipt message without an explicit migration decision and duplicate prevention. A service need is not automatically a supported transactional play. Check the current contract: Barua's transactional purposes are receipt, shipped, delivered, delayed, cancelled and refunded order messages. Other custom service needs require a supported sender or a recorded capability gap. Never work around consent by relabeling a marketing play. Unknown payment is not a confirmed failure; a queue acceptance is not fulfilment; missing tracking is not proof that a person did nothing. ## Trace facts through every decision Before choosing event definitions, complete these checks against the source inventory and all opportunities that use each fact. Record the resulting rules in the existing `facts`, `eligibility`, `stops` and `dependency` fields. Keep observed facts separate from the proposed decision to send a message. - Define a shared fact from the source operation that establishes it. Trace every consumer, including stops and externally owned messages. Put a message's audience filter in that opportunity's eligibility. If an event is deliberately narrower than the source fact, every other consumer needs its own complete observation. Recheck all consumers whenever a definition changes. - Evaluate current state at entry and again before sending. Include relevant activity before the trigger and work that is still unresolved. A stop that only observes later events cannot exclude an already completed action or an existing operation. Name the exact state observation and how it stays current; a missing observation is an implementation dependency, not evidence of absence. - Follow each proposed action URL through the actual destination and its server checks. Record the access and readiness conditions needed for the promised action to work. Preserve the meaning of an upstream completion fact when a downstream prerequisite is missing. Hold that message, use a verified usable destination, or propose the missing observation before claiming readiness. - Examine time conditions without a new callback. For each relevant deadline or waiting state, advance time while leaving stored state unchanged. Record whether a decision is needed and which current-state observation would support it. A changed timestamp and an elapsed timestamp are different conditions. Retain a justified exclusion or an explicit dependency when no observer exists. Trace each required observation into the exact event fields or an authoritative read the consuming system can actually perform. A proposed read needs a named implementation dependency until it exists. Keep material conditions in the compiled per-Play request, or in the generated playbook for a legacy artifact. A source reference or private note cannot replace the rule the message consumer receives. Copy facts and usable URLs must also reach the shared playbook or the approved copy fields that the email writer actually reads. ## Check coverage before plan operations In the second pass, review the map against the separate source inventory. Do not only reread the opportunities already selected. Every material route/state, relevant failure/recovery outcome and existing sender must reach a documented decision. Check source claims and use applicable counterexamples from this checklist: - The customer used a different creation mode or left a different task unfinished. - Another object completed while the triggering object still needs attention. - Payment remains unresolved after the proposed delay. Elapsed time alone does not establish failure or permission to start another purchase. - A failed operation returned to pending or processing, or later recovered. - Several distinct successful operations accumulate for the same object. Check the total and avoid counting retried or unsuccessful operations. - An existing sender already sent the same purpose for that occurrence. - Consent was refused, access changed or the account/object was deleted. Check that the proposed trigger, subject, eligibility, timing, stops, reentry and required context handle those cases. Field presence and valid event JSON do not prove this. Compare material facts, including amounts and product promises, with the source and with both artifact views. This author check prepares the strategy for the required independent review below. Set `coverage_review.status` to `complete` only after that pass, with its time and any explicit remaining dependencies. An unassessed area or unsupported claim leaves it `incomplete`: finish that discovery before proposing or approving the intended plan. A complete coverage entry still needs a passing independent review before submission. An assessed blocked, excluded or externally owned row is valid coverage. Hold its affected action or play, while continuing unaffected work the grant allows. Do not turn a dependency into a new owner checkpoint under full delegation. Now derive the full intended event plan and playbook from the map. Preserve existing events/declarations unless their removal is intentional and reviewed. Missing instrumentation stays on the implementation list; it must not narrow the desired journey. The intended schema can be approved before emitters are deployed. That does not make dependent plays ready to launch. Before submitting either input, check its actual claims against the source. An internal retry handler does not prove a customer retry exists or costs nothing. An uncertain payment does not establish failure. Label proposed message rules as proposals and keep unknown product promises out of the input. Prepare the intended plan input and brand patch. Generate the playbook from the recorded strategy before asking the independent reviewer to compare it: ```sh umask 077 node .agents/skills/barua-setup/scripts/journey-review.mjs --render playbook \ --journey-review-file /private/authorized/journey-review.json \ > /private/authorized/playbook.txt ``` Copy those exact UTF-8 bytes into `input.playbook` when building the action JSON. The output is already trimmed and has no final newline. In `per_play` mode it contains each covered row's kind, context, facts and sending owners. Complete message rules go into the individual requests below. A legacy artifact without the mode keeps all documented covered rules in this playbook, including implementation work and planned events. The playbook keeps labeled plain text, with shorter repeated labels to save space. `Sends now` records current ownership; `Send via` records proposed ownership. Field values and their line boundaries remain intact for email facts, links and human review. Blocked and excluded rows begin with an instruction not to create a Play; controls and existing sender ownership remain explicit. Blocked and excluded rows' complete intended rules remain in the private JSON and Markdown. The playbook exports only their outcome, ownership, reason, dependency and next action, with journey and kind labels. Put constraints needed to interpret the hold in that summary. If another consumer needs the same fact or condition, record it in that consumer's actual rules too; a held row's private rules do not reach that consumer. Resolving a hold requires a new render and review. The generated playbook must fit both existing limits: 24,000 JavaScript characters and 32,768 UTF-8 bytes. The latter prevents the proposal consumer from cutting off its end. An oversized result is refused, with both sizes and the largest rows. New artifacts use `per_play` so complete message rules do not compete for this shared space. Preserve every opportunity and material condition; do not drop or merge distinct decisions to fit. Regenerate and measure before preparing the review packet, then repeat the independent comparison if inputs changed after review. Nothing is truncated or silently replaced. The compiler omits evidence arrays and source revisions, but field text can still contain inline citations. Inspect the generated text before submitting it. Measure all rows as you draft, before preparing the packet: ```sh node .agents/skills/barua-setup/scripts/journey-review.mjs --render size \ --journey-review-file /private/authorized/journey-review.json ``` This returns every row's character and byte sizes, shared overhead, remaining capacity and `fits`. It succeeds for an oversized draft so you can plan one concision pass across the whole strategy. It does not approve or change it; playbook and packet rendering still refuse oversize content. Put supporting explanation in evidence, keep consumer conditions explicit, and avoid repeating the same rationale in outcome, reason and dependency. Preserve all material rules. Reserve time for final review, corrections and the permitted API/draft work rather than treating a finished private document as the outcome. ### Preserve complete rules in each Play request Use the new template's `authoring_mode: "per_play"` before drafting. Record an `execution_target` on every opportunity: - `{"type":"marketing"}` declares a covered Barua marketing Play, with its normal consent requirements. A service need can use this lane only when the reviewed ownership and consent rules allow it. - `{"type":"fixed_service","purposes":["receipt"]}` declares one or more of Barua's fixed purposes: `receipt`, `order_shipped`, `order_delivered`, `order_delayed`, `order_cancelled`, `order_refunded`. Review each actual purpose's revision separately. These are never marketing proposals. - `{"type":"none"}` is required for held, external and control decisions. It does not authorize a Play proposal or approval. The target is a reviewed declaration, not evidence that the sender or consent choice is correct. Independent review compares it with the full source and rules. The helper refuses executable targets on held or control rows and keeps exact approval targets in the declared lane. The packet adds `play_requests`, one entry per marketing target. Each contains `opportunity_id`, `action: "play.propose"` and `input`. All original message-rule fields and every covered control are included. Controls retain their scopes; unrelated message rows are not implicit dependencies. Expand any required condition from another row into the consuming row before review. The packet contains the exact complete requests, not summaries or extra reviewer digests. Each request must fit the existing limits: intent 4,000 characters, audience 2,000, trigger and goal 1,000 each, and at most 12 requirement strings of 1,000 each. Complete intent JSON must fit 24,000 UTF-8 bytes. Characters use JavaScript string lengths. The helper refuses chunks that trimming would change, and a complete packet over 512 KiB. An over-limit request is a transport limitation; do not remove conditions, controls or decisions to make it pass. After plan approval and brand confirmation, inspect any automatically prepared drafts against the complete requests. Shared factual context alone does not establish their intended triggers or stops. Adopt only an exact match. For a missing supported marketing Play, select its packet entry, remove only the `opportunity_id` wrapper field, and add the current `input.expectedToken` from `play-catalog`. Keep the other input values and strings unchanged. Calculate the ordinary action digest, record the source-based claim review and regenerate Markdown. Both the local action check and `manage.mjs action` require `--journey-review-file --opportunity-id `. The helper recompiles the request and refuses a mismatch before HTTP. Propose one Play, wait for its operation, then read the current catalog before the next proposal. The returned Play ID may differ from the opportunity ID; record that mapping and inspect its exact rules and every email example before approval. A test fixture with no authoring worker cannot prove these requests complete: inspect its supplied drafts and report other work unfinished. Creating a new Play does not replace an approved one. Small corrections use the existing feedback action; do not squeeze a full replacement into its 4,000-character note or retire a live Play to work around that limit. ## Prepare the review packet and task Prepare a private packet input with `plan_input` (the full intended proposal) and `brand_patch` (the exact intended `input.patch`). `plan_input` contains only `events` and optional `not_emitted`; include an empty `not_emitted` array when the action requires it. A plan-only packet may use an empty brand patch. The source-first `checklist` is optional at packet creation. Calculate the bindings without workspace credentials: ```sh node .agents/skills/barua-setup/scripts/journey-review.mjs --render packet \ --journey-review-file /private/authorized/journey-review.json \ --input-file /private/authorized/review-packet-input.json \ > /private/authorized/review-packet.json ``` The packet contains `strategy_sha256`, `plan_input`, `plan_sha256`, `brand_patch`, `brand_patch_sha256`, the generated `playbook` and `playbook_sha256`. In `per_play` mode it also contains the exact `play_requests`. The reviewer checks each request and target against the linked opportunity and all applicable controls. Supplying `checklist` also returns `checklist_sha256`. If your harness supplies `BARUA_SETUP_REVIEW_CONTEXT_FILE`, use that private context file unchanged as the task formatter's `--input-file`. It contains the trusted owner request, complete frozen source paths, canonical artifact paths and configured model/effort. Write the strategy and packet at its supplied paths. Do not shorten its owner request, remove source files or replace its settings. This is nonsecret task metadata, not model authentication. The local evaluation retains both expected tasks before author execution and checks completed source-only discovery followed by a distinct fresh final reviewer. Editing the context file cannot change those expectations. Without supplied harness context, create a private task context JSON with exactly these fields: | Field | Value | |---|---| | `owner_request` | The owner's actual request, without the author's strategy or conclusions. | | `source_files` | An array of product source file paths for independent discovery. | | `strategy_file` | The path to the complete `journey-review.json`. | | `packet_file` | The path to the generated review packet. | | `model`, `effort` | The harness-reported model ID and configured effort, using `BARUA_SETUP_MODEL` and `BARUA_SETUP_EFFORT` when supplied. | Use absolute paths that the reviewer can read. Generate early discovery from the installed [discovery task](https://barua.ai/skills/barua-setup/templates/journey-discovery-task.md), before the strategy and packet exist. It exposes only the owner request, source paths and configured metadata. Generate the final task from the installed [reviewer task](https://barua.ai/skills/barua-setup/templates/journey-review-task.md) once the packet exists. It also reads the shipped [event contract](https://barua.ai/skills/barua-setup/references/events.md) after its own source-first checklist. Normal installation includes both templates and the contract. For manual downloads preserve their paths relative to the helper: ```sh node .agents/skills/barua-setup/scripts/journey-review.mjs --render discovery-task \ --input-file /private/authorized/reviewer-context.json \ > /private/authorized/discovery-task.json node .agents/skills/barua-setup/scripts/journey-review.mjs --render reviewer-task \ --input-file /private/authorized/reviewer-context.json \ > /private/authorized/reviewer-task.json ``` The result has `prompt`, `expected_model` and `native_call`. Pass `prompt` unchanged to native delegation. In the verified `native_call` shape, `subagent_type` is `general-purpose`, `effort` is the configured value, and `run_in_background` is `false`. Apply these requirements using the corresponding controls your native harness supports; do not invent arguments or silently change a setting. Record `expected_model` verbatim as the configured model declaration. The harness must verify the completed reviewer's actual resolved model from native runtime evidence; that metadata may be retained outside the tool text shown to the author. Never append an explanation to the model string. If neither visible evidence nor a trusted harness verifier can establish the actual model, report that limit and hold the affected submission. Task generation only formats instructions. It does not start a reviewer or supply model authentication. ## Obtain the independent review Use your agent harness's native delegation to start a separate reviewer with fresh context and the same configured model and effort. Verify that this capability is available; the skill does not install a reviewer or supply model authentication. Do not inherit the author's strategy or conclusions into the reviewer's initial context. Keep the reviewer read-only, without workspace credentials, customer records or permission to change product files or Barua. Start a fresh final task, separate from the discovery agent, with the generated prompt and call requirements. Do not resume a previously used reviewer or inherit the author's conversation or early discovery. Keep the prompt intact, including its direct `Read`, `Grep` and `Glob` restrictions; do not replace them with shell commands. Wait for the completed foreground result before submitting. A started task or progress message is not a completed review. The canonical task requires a source-derived plain JSON checklist before any artifact reads, then comparison of the complete strategy and intended inputs. The reviewer emits the checklist as an intermediate text block and immediately follows it with the event-contract `Read` in the same response. Returning the checklist ends the native task before comparison; it is an incomplete review. Only the final judgment completes the task. For this recorded protocol, the reviewer must use `Read` on `strategy_file`, the actual journey JSON. Its matching generated Markdown remains the full human-readable view. The final result uses the task's exact top-level receipt fields; do not add `CHECKLIST` or `RESULT` labels, a nested `bindings` object, or a prose reconstruction. Retain the ordered native tool/session evidence and both JSON outputs. The parent may attach only harness-reported `author` and `reviewer` identity, `checked_at` and the calculated `checklist_sha256`. Preserve the reviewer's remaining result, including findings and input bindings. Retain the reviewer's result. For strategy corrections against unchanged product sources, prepare the current packet and start another fresh final reviewer with the same canonical task. Each independently derives its source checklist; do not supply a previous review's conclusions as its starting point. Keep every failed result. Material source changes require fresh early discovery too. If the required independent review is unavailable or cannot finish, keep the affected strategy submissions on hold and report that limit. Continue unaffected work permitted by the grant. Do not substitute self-review, add a human checkpoint, request another model account or silently start a nested CLI/API call. The owner may still choose a human checkpoint through the existing permission policy. The local helper checks recorded review fields and content bindings. It does not inspect the source tree, verify that another agent ran, or certify the reviewer's conclusions. Preserve the actual delegation/session evidence for those execution claims. Recheck current source before submitting; record relevant uncommitted changes with content hashes in source metadata rather than reusing a commit identifier for different content. Use the author session ID and reviewer agent/session ID reported by the harness, with its exact configured model and effort declarations and retained runtime verification. When supplied by the harness, `BARUA_SETUP_SESSION_ID` identifies the author session; it is not a credential. The harness may also supply `BARUA_SETUP_MODEL` and `BARUA_SETUP_EFFORT`; use those or its reported equivalents without asking the owner to configure new values. Do not invent identifiers, substitute an unverified alias or put a visibility caveat inside an identity field. If the harness cannot supply the required identity, report that the review cannot be recorded yet. ## Bind and submit the reviewed inputs Once the reviewer has emitted its initial checklist, the packet command can calculate its digest without changing the other packet fields. Keep the reviewer's original expectations and evidence unchanged. Record the passing result in `independent_review` only after all material findings are resolved: | Field | Required content | |---|---| | `version`, `status`, `checked_at` | `1`, `pass` and an ISO review time with a timezone. | | `author`, `reviewer` | Each has `session_id`, `harness`, `model` and `effort`. Sessions must be distinct; use the configured model and effort. | | `strategy_sha256` | The packet's strategy digest, including recorded sources and all semantic fields. | | `plan_input`, `plan_sha256` | The exact reviewed event proposal and its packet digest. | | `brand_patch`, `brand_patch_sha256` | The exact reviewed brand patch and its packet digest. | | `playbook_sha256` | The packet's generated playbook digest. | | `checklist_sha256`, `checklist` | The final reviewer's source-first digest and rows with `id`, `current`, `expected`, `prerequisites`, nonempty source `evidence`, linked `opportunity_ids` and `verdict: "pass"`. The digest covers every field except the comparison's opportunity links and verdicts. Existing three-field discovery digests remain readable. | | `counterexamples` | Rows with `id`, `scenario`, `expected`, source-backed `result`, nonempty source `evidence`, linked `opportunity_ids` and `verdict: "pass"`. | | `findings` | An empty array for a passing result. Retain earlier findings separately rather than deleting the failed review evidence. | Both row lists must be nonempty, with distinct IDs and valid opportunity links. The completed checklist must cover every opportunity, including held and excluded decisions. Changes to source evidence, intended rules, the plan, brand patch or generated playbook require another independent comparison. The strategy digest omits only top-level `captured_at`, `discovery_review`, `coverage_review`, `workspace`, `reconciliation`, `approval_observations`, `submission_reviews` and `independent_review`. All other fields, including additions and recorded source revisions, remain bound. Updating only those operational records does not require another semantic review. Brand version/checkpoint fields `expectedVersion`, `proposalHash` and `seen` are outside the independent semantic binding; the exact action review below still binds their current values. Other brand input fields are refused as unreviewed. Save the exact `{action,input}` JSON and calculate its digest with `node /scripts/journey-review.mjs --input-file `. Add a `submission_reviews` row with `action`, `input_sha256`, `status` set to `complete`, `checked_at` and `claims`. Each claim has `text`, `status`, `evidence` and `disposition`. Status is `observed`, `proposed` or `unknown`; disposition is `included` or `omitted`. Observations need source references. Unknown claims must be omitted. Review the exact playbook prose as well as the intended message actions; a digest only binds the input you checked. Then regenerate the Markdown to include this submission review. The order is packet and playbook render, independent source-first checklist and comparison, action construction, action digest and claim review, Markdown render, then submission. Playbook rendering never reads submission reviews, reconciliation, approval observations, independent review or workspace metadata, so recording a digest cannot change the input it binds. For onboarding plan proposal, plan approval and playbook actions, pass `--journey-review-file` to `manage.mjs action`. Before HTTP it checks the current generated Markdown and recorded independent review with matching inputs. Brand actions also require the exact generated playbook before the input digest and claims are checked. The helper reads both files as bounded regular files without following a file symlink; it never writes or repairs them during submission. Plan approval also reads the current server plan and compares its full normalized definitions and declarations with the reviewed `plan_input`; event names alone do not establish a match. An already approved plan is accepted for retry only when there is no current proposal and its version matches both the requested version and the workspace's plan version. A newer proposal requires review of that proposal. After a successful proposal, record its returned version in `workspace.plan_version` before requesting approval. This records the selected plan version; it does not claim the plan is already approved. Keep the artifact and request unchanged during a same-key retry; refreshed operational inputs need their own exact action review. For event-only access, use the local `journey-review.mjs` command with both file flags to check the same wrapper and current Markdown, then submit the plan through the usual integration API. Send only the plan input there, without the local wrapper. Direct plan and brand APIs do not certify source coverage. Unrelated ongoing brand settings changes keep their normal API path. Event-only brand token proposals still need source-grounded independent review, but this helper does not validate the `PUT /brand` wire body; preserve its existing source-token contract. ## Carry the intent into actual plays Keep stable opportunity IDs and every material rule in the artifact. With `per_play`, the compiled requests carry those rules; the playbook carries shared context. Legacy artifacts retain rules in the playbook. If an input limit prevents representing the intended scope, report that limit and keep the full artifact; never silently submit a smaller strategy as complete. Use the authoring path the workspace supports: - Default play creation: the confirmed or updated brand playbook feeds Barua's automatic proposals. In `per_play` mode that input has shared context only; inspect the resulting catalog and reconcile exact revisions against the full opportunity rules. Automatic generation does not establish rule coverage. - With confirmed brand context, Play review and the connection's proposal permission: use the compiled `play.propose` request for a new Play, then wait for the exact prepared revision. Change an existing Play through `play.request-changes` with its current token. A new proposal does not replace or stop an existing Play. Barua writes a sequential Play when branching is disabled and a graph when enabled. Do not enable a feature flag or write a graph to bypass unavailable capabilities. Before approving any play, reconcile its actual rules and rendered emails against the relevant opportunity. Check branch outcomes and waits as well as eligibility, subject identity, stops and reentry. Simulate representative and stopping cases when the current API supports it. A successful authoring job alone does not pass this review. Each `reconciliation` row has `opportunity_id`, `play_id`, `revision_id`, `content_hash`, `role` (`proposed` or `approved`), `review_token`, `status`, `checks` (specific observed findings) and `checked_at`. Use `matches`, `mismatch`, `missing`, `unavailable`, `external` or `excluded` as status. IDs/hashes are null when no revision can be read; never invent them. Role and token are null when no current play can be read. Use the current play detail's `summary.token`. Record a separate row for each proposed/approved role: different policies can share the same email revision and content hash. `matches` requires inspecting that role's rules and the revision's samples. An approved match cannot stand in for changed proposed rules. Refresh this check when the token changes; do not infer it from the name or older approval. Management approval also needs a separate `approval_observations` row. Read the exact target with `manage.mjs play --play-id --revision-id `. Review `approvalTarget.play` together with `selected.flow` and the selected revision's synthetic email previews. The target is the policy approval would execute; `selected.play` can have different display or history semantics. Do not calculate a policy hash yourself or infer the target from its name. The observation has `play_id`, `revision_id`, `content_hash`, `policy_hash`, `review_token`, `status` and `checked_at`. Copy the target's hashes and the current summary token. Use `matches` only after inspecting the target and emails. Add `opportunities` as `{id,checks}` rows, with distinct covered opportunity IDs and specific findings. Each finding fits within 2,000 characters. Add `previews` as `{stepId,languages}` rows covering every selected email step and every requested language, without duplicates. Keep current proposed/approved display comparisons in `reconciliation`. When the canonical target is the current proposed policy, a matching proposed row is required too. Historical or additive-language targets use their own canonical observation; do not relabel an unrelated display policy. Older artifacts remain readable. Add the new observations before a new approval. Use `manage.mjs action --journey-review-file ` with the exact approval input and the current generated Markdown sibling. The helper rechecks the canonical target and renders every requested step/language example. Missing renders or lint failures stop submission. These checks do not replace your copy and policy review or write findings for you. The API receives the artifact digest and compact evidence; the full inventory stays local. Keep the action and artifact bytes unchanged when retrying a lost response, and reuse the idempotency key. If the review token advanced, the helper can resend the original record for server replay. A new stale-token request cannot grant approval. A failed preview under the current token never takes that recovery path; review the failure, then retry the same request. The helper shares its existing deadline across detail, previews and submission, with a 2 MiB reply limit and a 16 MiB total read limit. It never approves a partial set of requested previews. Missing drafts remain `missing`; inaccessible drafts are `unavailable`. Request changes for a mismatch through the permitted API, then review the new revision. A failed request is still unfinished work. Do not approve a mismatch, invent unavailable permissions, or claim that an unsupported revision cycle worked. Record the inspected revision and its findings immediately, including a held revision or failed change request; do not leave the artifact at its preproposal state while retrying. Event-only agents retain the intended play rules and report the exact owner steps; they do not claim to have inspected inaccessible plays. Recheck this mapping before launch. A covered opportunity may still be unimplemented, an older approved play may still be active, and an approved replacement does not itself prove live delivery. ## Resume and hand off Refresh affected rows when product facts, scope, the event plan, play versions or sending ownership change. Read current versions before resuming approval. Before any final handoff, update the observed workspace plan version and reconciliation rows, then use `--render markdown` to regenerate that view. Do this for an incomplete run too. Keep the original source observation time distinct from later checks. An old approval cannot retrospectively establish that journey coverage was reviewed. Do not automatically revoke approvals or pause existing sending; use the owner's requested scope and current grant. Link the current `journey-review.md` and `journey-review.json` beside the [observed review package](https://barua.ai/skills/barua-setup/references/review-package.md), or the event-only proposal. The journey artifact includes planned and missing work; the package contains what Barua actually stores. Read both before reporting completion. Keep the artifacts available at the authorized private location. Report **strategy coverage** and **operational readiness** separately, naming the remaining dependencies. Follow the normal exact-operation checkpoint and fresh delivery-proof procedure. Do not call a resumed workspace a fresh test, or a complete event payload a complete onboarding strategy. --- Source: https://barua.ai/skills/barua-setup/references/management.md # Manage a Barua workspace For customer profiles, segments, stored emails, previews and diagnostics, see [inspection.md](https://barua.ai/skills/barua-setup/references/inspection.md). It lists each read, helper and required scope. Use this page for setup and ongoing changes with an owner-authorized management connection. For emitting product events, use [events.md](https://barua.ai/skills/barua-setup/references/events.md). Workspace creation, membership administration, billing and workspace deletion are outside this API. For the owner's account, workspace creation, team and exact reviews, use [owner.md](https://barua.ai/skills/barua-setup/references/owner.md) and `scripts/owner.mjs`. That separate owner API preserves admission and membership checks and does not require opening the app. Enabled pixel observations can inform and trigger marketing plays for identified people under the existing marketing basis. Server events remain authoritative for consent and transactional email. ## Access and discovery `BARUA_MANAGEMENT_KEY` is a workspace-specific `mk_` secret. It authorizes `https://barua.ai/api/management/v1`; an `ik_` event key cannot call that API. The owner creates the connection through Settings → Agents or the owner API below. The owner selects permissions and review checkpoints. An expiry is optional. `review: []` delegates the permitted actions without an extra review. Checkpoints can be any of `plan`, `brand`, `play`, `launch`, `campaign`, and apply to later changes too. Full delegation does not add permissions. Check the actual grant before saying that launch is allowed. Do not substitute browser clicks for a missing permission. Management keys have no test or production mode. Their permissions apply to the real workspace, including live intake and sending when launch is granted and setup requirements are met. Explain this before asking an owner to save the key. Store it as `BARUA_MANAGEMENT_KEY` in the agent's secret store and load it into the helper's process environment; `manage.mjs` does not read env files automatically. Establish a secure destination and a permitted transfer method before creating a connection. If browser transfer is unavailable, arrange owner-assisted saving and keep the key panel open until saving is confirmed. Ask for only the saved location. Continue independent work until it is available, then verify access with `capabilities`. The screen's **Access granted** status (formerly **Connected**) confirms permission, not that the agent holds a key. Event keys are separate. Both modes use the `ik_` prefix and `BARUA_API_KEY`: the test event key belongs in development or CI, and the live event key belongs in production hosting secrets. Read each key's recorded `mode`; its prefix cannot distinguish test from live. Use test keys for emitter checks. A management key cannot replace either event key, and a test event key does not make management actions a dry run. Test events validate without creating people or sending email. Start with `GET /capabilities`. It returns the current connection policy and legal version. Each action has a JSON input schema alongside its permission and review status. Use those schemas when constructing a request. `GET /setup` returns current steps, facts, a readiness `version` and possible next actions. An integration API response named `needs_human` is a legacy checklist; management permissions determine whether this agent can perform its corresponding action. | Permission | Allows | | --- | --- | | `setup:write` | Event keys, tracking plan, brand, email layout, domain and delivery proof | | `play:write` | Play proposals and exact approval, feedback, retry, sending controls, journey cancellation and enabling play review | | `launch:write` | Enabling or stopping live intake and play sending; activating a prepared email provider | | `segment:write` | Creating, updating and previewing saved segments | | `campaign:write` | Creating, updating and preparing campaign drafts with Barua | | `campaign:send` | Sending, pausing, resuming and cancelling campaigns | | `history:write` | Authorizing runtime keys to import history | | `people:read` | Customer lists, current profiles, masked activity and real-profile preview; runtime keys with this permission | | `emails:read` | Saved email content and delivery evidence; also requires `people:read` | | `people:erase` | Erasure actions and runtime keys with this permission | | `marketing:attest` | Recording an explicit marketing-lawful-basis statement | A grant does not establish consent or another lawful basis. Supply a factual statement only from an owner or an agent specifically authorized to make it. A recipient's recorded refusal still wins. Current terms are required for changes. The stop actions `sending.pause`, `intake.disable`, `play.pause`, `play.entries.stop`, `play.run.cancel`, `key.revoke`, `campaign.pause`, `campaign.cancel`, and connection revocation, remain available when terms are stale. Other valid new or retried actions return `terms_required` before looking up a prior request or preparing workspace data. Resuming a non-stop operation also requires current terms, even if it already completed. Operation reads remain available so a fresh process can find the IDs needed to stop. Key lists, play details and campaign lists are also readable. All current workspace owners must accept the current terms before live intake can start. Accepting future legal versions is never implied by a connection. ## People without the UI `GET /capabilities` includes a `reads` map keyed by helper command. Each entry states its HTTP method, path and permission, whether this connection is allowed, and its pagination fields. These reads require current terms and `people:read`. | Helper command | API route | Input | | --- | --- | --- | | `people` | `GET /people` | Optional `--limit` and `--cursor` | | `people-search` | `POST /people/search` | Private JSON through stdin or `--input-file` | | `person` | `GET /people/:id` | `--id` with the person UUID | | `person-activity` | `GET /people/:id/activity` | `--id`, optional `--limit` and `--cursor` | | `person-events` | `GET /people/:id/sessions/:sessionId/events` | `--id`, `--session-id`, optional `--limit` and `--cursor` | Search JSON contains exactly one of `email` and `external_id`, with optional `limit` and `cursor`. Keep those private details out of URLs and command arguments. Search uses current identity evidence, including canonical email aliases. A still-current external-ID alias can find a person even when another source supplies their displayed ID. Retired identities do not count as matches. Pages default to 20 items and accept at most 100. Continue with `next_cursor` until it is null, keeping the same page size and filters. People are ordered by UUID; activity by newest session start. Pages reflect live data, so begin a new walk to see newly inserted rows behind an existing cursor. `person` returns identity plus `facts`, `canonical`, `pixel` and `suppression` sections. Check each section's `state` before using its `data`. `available` with null data means no retained result; `unavailable` means the read failed or exceeded its budget. `complete` is false if any section was unavailable. Never treat unavailable suppression as permission to send. Activity includes undigested sessions; use its session `id` with `person-events`. Events contain masked readable labels and decimal-string IDs, never replay objects or form values. These commands do not send email or run a model. Email bodies and email history are outside these people reads. ## Helper Run `node /scripts/manage.mjs `. Node 18.17 or newer is required; the script has no package dependencies. `BARUA_URL` defaults to `https://barua.ai`. Only HTTPS origins are accepted, except HTTP on localhost. Credentials come from environment variables, never command arguments. The example below assumes a Codex install under `.agents/skills`. Use the installed skill path for another agent. Set `BARUA_OPERATION_ID` to the UUID returned by the preceding action before waiting. ```sh node .agents/skills/barua-setup/scripts/manage.mjs capabilities node .agents/skills/barua-setup/scripts/manage.mjs setup node .agents/skills/barua-setup/scripts/manage.mjs action --idempotency-key plan-001 --input-file plan.json node .agents/skills/barua-setup/scripts/manage.mjs wait --id "$BARUA_OPERATION_ID" ``` For a core-only product, `plan.json` still contains a proposal: ```json {"action":"plan.propose","input":{"events":[],"not_emitted":[]}} ``` Create keys with an explicit mode and least privilege. For example, the action body for a test emitter is: ```json {"action":"key.create","input":{"mode":"test","label":"Development emitter","capabilities":["events:write"]}} ``` Key creation requires `--secret-file ` or `--secret-command-file `. The first reserves a new file with mode 0600 and refuses symlinks. Files inside Git must already be ignored. The second sends the new secret on stdin to the authorized command, without a shell. Its output is suppressed. Never place a secret in the argv array. For live keys, use the production secret store's already authorized command. A failed transfer is recovered with the same idempotency key and a new destination; it must not create another credential. `wait --id` also recovers a completed key creation into the requested destination, even when a previous process lost the successful response. `status` only reads metadata. If the key has been revoked or recovery is no longer permitted, a key command reports `secret_unavailable` instead of claiming the secret was saved. Each event-key mode has two active slots. Rotate by creating the replacement, installing it and proving fresh traffic before revoking the old key. Never revoke a working key just to make space without an approved rotation path. Keep secrets out of prompts, logs and commits as well as command arguments. A workspace allows 20 unrevoked, unexpired management connections. Expired records remain in history without using a slot; renewing one needs a free slot. The connection list places active connections before history and returns at most 100 records. An exact creation retry reuses its existing connection. | Command | Input and result | | --- | --- | | `connect` | Owner credentials; connection JSON; idempotency key and secret destination; returns connection metadata | | `recover-key --id ` | Original granting owner's credentials and a secret destination; saves the same active management key without changing policy or pending operations | | `connections` | Owner credentials; lists connections | | `capabilities`, `setup`, `operations` | Management credentials; read-only discovery and operation listing | | `review-package --output-dir ` | Reads setup, the full event plan, exact play revisions and synthetic previews; writes `index.html` and `review.json`. Optional `--input-file` adds source evidence; optional `--proof-id` reads an existing proof. See [review-package.md](https://barua.ai/skills/barua-setup/references/review-package.md). | | `segment-fields` | Approved `segmentFields` for audience rules and `mergeFields` for personalization | | `segments`, `campaigns` | Saved segment or campaign summaries; use `--after` for another page | | `segment --id `, `campaign --id ` | Current detail, versions and prepared previews | | `campaign-job --id ` | One background job's status, error and progress counts | | `campaign-recipients --id ` | Current prepared audience results; requires `people:read`; use `--after` for another page | | `action` | `{action,input}` JSON from stdin or `--input-file`; requires `--idempotency-key` | | `status`, `resume`, `cancel` | Management credentials and `--id` operation UUID | | `review` | Owner credentials with exact review JSON for the operation UUID | | `wait` | Polls and resumes one operation. Completed keys need a secret destination; pending checkpoints need owner review. | | `proof` | Proof UUID; `--bind-stdin` or `--input-file` reads `{ "eventId": "" }` and retries binding while intake is pending, then polls by UUID. Without either flag it only reads by UUID and ignores stdin. | | `play-catalog`, `play-job --id ` | Current proposal token and rollout readiness; exact preparation state | | `play --play-id ` | Review detail and current approval token | | `play-definition`, `play-validate`, `play-simulate` | Require `--play-id` and `--revision-id`; simulation takes synthetic timeline JSON from stdin or `--input-file` | | `play-runs`, `play-feedback`, `play-trace` | Require `--play-id`; trace also takes a run UUID with `--id`. Pass each returned cursor with `--after`. | | `play-paths` | Path counts for `--play-id` and `--revision-id`, including the measured window and completeness | `--timeout-ms` bounds the whole call or wait (1–300000, default 60000). It includes destination checks and secret storage. Each Git check or secret command gets at most 15 seconds or the remaining total time, whichever is less. At expiry the helper ends its child process and reports failure without waiting for that process's pipes to close. Recover any created key through the same request or operation; the timeout does not undo an external secret-store write. `--poll-ms` is 100–10000, default 1000. Ordinary output is one redacted JSON object. Exit codes are independent of `verify.mjs`: 0 succeeded/satisfied, 1 helper or transport error, 2 authorization refused, 3 terminal unsuccessful, 4 awaiting approval, 5 still pending at timeout. For `proof`, only delivered exits 0. Provider failure exits 3; unconfirmed delivery exits 5. A timeout is not cancellation. Keep the operation UUID and read or resume it later. List commands `operations`, `segments`, `campaigns` and `campaign-recipients` accept `--after `. Pass the exact returned `next` until it is null; the helper reads one page per call. A campaign job is read by its returned UUID, not by a page cursor. These reads never consume stdin or submit an action. For `play.propose`, `play.request-changes` and `play.retry`, `wait` follows the saved preparation job after the operation succeeds. It exits 0 only when the job returns a ready revision. `action` acknowledges the operation; it does not wait for authored structure or email copy. Preparation states are `accepted`, `preparing`, `ready` and `failed`. A timeout retains the same job for later reads. ## HTTP contract Fetch `/capabilities` for each action's current input schema. A checkpoint applies only when the connection's `review` array contains it. A blank checkpoint means the permission alone authorizes it. | Action | Permission | Checkpoint | | --- | --- | --- | | `segment.create` | `segment:write` | | | `segment.update` | `segment:write` | | | `segment.preview` | `segment:write` | | | `campaign.create` | `campaign:write` | | | `campaign.update` | `campaign:write` | | | `campaign.prepare` | `campaign:write` | | | `campaign.send` | `campaign:send` | `campaign` | | `campaign.pause` | `campaign:send` | | | `campaign.resume` | `campaign:send` | `campaign` | | `campaign.cancel` | `campaign:send` | | | `key.create` | `setup:write` | | | `key.revoke` | `setup:write` | | | `plan.propose` | `setup:write` | | | `plan.approve` | `setup:write` | `plan` | | `brand.confirm` | `setup:write` | `brand` | | `brand.update` | `setup:write` | `brand` | | `template.import` | `setup:write` | `brand` | | `domain.provision` | `setup:write` | | | `domain.verify` | `setup:write` | | | `domain.activate` | `launch:write` | `launch` | | `play.propose` | `play:write` | | | `play.approve` | `play:write` | `play` | | `play.discard-proposal` | `play:write` | | | `play.retire` | `play:write` | `play` | | `play.request-changes` | `play:write` | | | `play.retry` | `play:write` | | | `play.pause` | `play:write` | | | `play.resume` | `play:write` | `play` | | `play.entries.stop` | `play:write` | | | `play.entries.start` | `play:write` | `play` | | `play.run.cancel` | `play:write` | | | `review.enable-plays` | `play:write` | `play` | | `intake.enable` | `launch:write` | `launch` | | `intake.disable` | `launch:write` | | | `sending.enable` | `launch:write` | `launch` | | `sending.pause` | `launch:write` | | | `proof.start` | `setup:write` | | | `marketing.attest` | `marketing:attest` | `launch` | | `person.erase` | `people:erase` | | Send `Authorization: Bearer $BARUA_MANAGEMENT_KEY` and JSON. The key selects the workspace. `X-Workspace-Id`, if supplied, must match it. Management reads never return raw credential secrets. `POST /actions` and key-creation resume may return a recoverable secret for that connection's own key; consume it through the helper. | Route under `/api/management/v1` | Behavior | | --- | --- | | `GET /capabilities` | Current action schemas and grant | | `GET /segment-fields` | Current `{segmentFields,mergeFields}` catalogs; use their exact paths and types | | `GET /segments`, `/segments/:id` | Saved segments and current detail; lists take `after` and return `next` | | `GET /campaigns`, `/campaigns/:id` | Campaign summaries or full prepared detail; lists take `after` and return `next` | | `GET /campaigns/:id/recipients` | Prepared audience results; takes `after`, returns `next`, and requires `people:read` | | `GET /emails` | Saved email metadata; requires both `people:read` and `emails:read`. Accepts `status`, `person_id`, `decision_id`, `campaign_recipient_id`, `play_id`, `limit` (1–100; default 20), and the returned opaque `cursor`. Returns `emails` and `next`. | | `GET /emails/:id` | Saved email detail and delivery evidence. Exact accepted submissions are distinguished from prepared payloads and legacy saved drafts with an unknown historical recipient. | | `GET /emails/:id/preview` | Inert text HTML; cannot load tracking resources or follow links. Same two permissions. | | `GET /emails/:id/raw` | Explicit raw HTML attachment. Save privately; do not open it as an active page or paste it into chat. Same two permissions. | | `GET /campaign-jobs/:id` | One preparation, segment preview or send job's status and progress | | `GET /setup`, `/plan`, `/events/status`, `/brand`, `/keys`, `/domain` | Current setup and configuration; domain read does not verify DNS or flush sending | | `GET /templates/inbound`, `/templates/inbound/:id` | Forwarding address and existing email layouts; requires `setup:write` | | `GET /plays`, `/plays/:id/detail` | Play list and exact review detail; the detail route supports every valid play ID | | `GET /plays/mode`, `/plays/map`, `/plays/results` | Current review mode, play map and aggregate results | | `POST /plays/profiles` | Customer lookup with `{email,after?}` in the private JSON body; requires `people:read` | | `GET /plays/:id/template` | Read the play's email template | | `POST /plays/:id/preview` | Render preview; a real customer profile requires `people:read` | | `GET /plays/catalog`, `/plays/jobs/:jobId` | Read proposal readiness or follow a saved preparation request | | `GET /plays/:id/definition` | Exact immutable definition and content hash; takes `revisionId` | | `POST /plays/:id/validate`, `/plays/:id/simulate` | Validate an existing revision or simulate its synthetic timeline | | `GET /plays/:id/runs`, `/plays/:id/runs/:runId` | Paginated person journeys and their transition evidence; requires `people:read` | | `GET /usage` | Monthly workflow starts, allowance and enforcement state. A null allowance means unassigned, not zero. | | `GET /plays/:id/admissions` | Starts waiting for allowance or settled before a run began; requires `people:read`. Supports `status` and `after`. | | `GET /plays/:id/feedback`, `/plays/:id/path-counts` | Feedback lineage and per-revision path counts | | `GET /operations` | Summaries of this connection's durable work | | `GET /operations/:id` | One full operation, including its prepared input and result | | `POST /actions` | Submit `{action,input}` with `Idempotency-Key` | | `POST /operations/:id/resume`, `/operations/:id/cancel` | Continue or cancel the same operation | | `GET /proofs/:id`, `POST /proofs/:id/bind` | Read proof or bind `{eventId}` in the request body | `GET /plan` includes canonical core event definitions alongside proposed and approved custom plans. The review-package helper requires those definitions so it cannot present a custom-only plan as complete. Use the literal play routes above; encoded play-route aliases are refused. Play detail also remains available at `/plays/:id` for compatibility, except IDs `mode`, `map`, `results`, `profiles` and `catalog`, whose names are reserved for discovery routes. Use `/plays/:id/detail` for all IDs to avoid those collisions. Management refuses GET profile lookup so email addresses stay out of its URLs. Play reader responses have `canReview: false` because that field controls the browser's owner buttons. It does not describe your delegated authority. Read `/capabilities` for your permitted actions and required checkpoints, and use `/actions` for changes. Customer lookup and previews using `personId` require `people:read`. For play pages, pass the returned `next` as `after` together with the returned `revision`. Detail accepts optional `revisionId` and `afterVersion`. Template reads require `revisionId`, `stepId` and `language` query parameters. Preview uses those same three fields in JSON plus exactly one of `example: true` or `personId`; its body limit is 1 KiB. Profile lookup also has a 1 KiB body limit and returns `next`, which becomes `after` in the next body. Results take one to 12 repeated `playId` query parameters. Run and feedback pages return at most 20 rows and an opaque `next` cursor; pass it as `after`. Trace pages use the same contract in execution order. Path counts take `revisionId` and cover the latest 30 days, with at most 10,000 transitions. `complete: false` means the counts cover that bounded sample, so do not report them as the whole window. Existing send and goal metrics retain their current meaning. ## Propose and operate a Play Read `play-catalog` for `expectedToken` and `branchingEnabled`. With confirmed brand context and Play review enabled, send `play.propose` with that token and a plain-language `intent`. Branching disabled produces a sequential Play; branching enabled produces a graph. Optional `audience`, `goal`, `trigger` and `requirements` make the intended behavior precise. Barua writes the structure and every email. Raw graphs and email templates are not accepted as proposal input. The complete intent content must fit within 24,000 UTF-8 bytes. For onboarding, use the exact request in the reviewed packet's `play_requests`. Pass `--journey-review-file` and its `--opportunity-id` to `manage.mjs action`; follow [the per-Play workflow](https://barua.ai/skills/barua-setup/references/journey-review.md#preserve-complete-rules-in-each-play-request) to bind the current token and action review. Submit one proposal at a time, wait for its revision, then refresh the catalog before the next proposal. `play.propose` creates a new distinct Play. To change an existing Play, use `play.request-changes` with its current token and feedback. Creating another Play does not replace or stop the previously approved one. Use the returned operation ID with `wait`, then read its ready revision. `play-definition` returns the complete definition, exact content hash and readiness blockers. `play-validate` validates that saved revision. Simulation accepts `{revisionId,input}` over HTTP; the helper wraps its input file using `--revision-id`. The synthetic input includes `startedAt`, `language`, `values` and optional `until`, `tracking` and `timeline`. Timeline items are either `{at,type:"event",name,properties?}` or `{at,type:"message",step_id,event:"opened"}`. Message events may also be `clicked` or `delivered`. Use made-up values. Simulation never reads a customer profile or sends mail; its assumptions describe the delivery checks it omits. `values` uses nested objects. Dotted rule paths are not literal object keys. For a signup example, supply: ```json { "startedAt": "2026-10-20T12:00:00Z", "language": "en", "values": { "events": { "signed_up": { "count": 1 }, "activated": { "count": 0 } } } } ``` Use the exact language returned by the saved revision. Read the simulation's entry decision before interpreting later paths; a person who does not meet the entry rule will not reach an email or branch. Open reports describe a recorded tracking signal, not proof that a person read the email. Click reports are currently disabled for workspace mail. Readiness reports a click-dependent path as blocked; absent click collection cannot count as no engagement. Use a supported signal or request a revised Play. Approve the exact revision and languages with the current review token. Approval does not enable sending. Existing journeys stay on their approved revision when you approve an update. New delegated `play.approve` inputs also require `reconciliation`. Run the action helper with `--journey-review-file `. Follow [journey-review.md](https://barua.ai/skills/barua-setup/references/journey-review.md) to record the reviewed canonical approval target and complete synthetic preview coverage first. The helper reads that artifact and current server state, then attaches the bounded record. It does not upload your source files or private Markdown. For a selected historical or additional-language revision, inspect `approvalTarget.play` with `selected.flow`. `approvalTarget.policyHash` names the policy canonical approval will use; `selected.play` may describe a different historical approval. `manage.mjs play --play-id --revision-id ` reads the exact version. Never derive a policy hash locally. The record carries `schemaVersion: "barua-play-reconciliation@1"`, `artifactSha256`, `contentHash`, `policyHash`, opportunity IDs with specific `checks`, and a `previews` list of step IDs and reviewed languages. It is stored with the operation and bound by its existing input hash. The server checks the current target and complete requested preview matrix at submission and again when reviewed or resumed. It cannot certify that an agent understood the source or that a person read the report. Full delegation still runs without a pause. Reconciliation refusals include `reconciliation_required`, `reconciliation_invalid` and `reconciliation_changed`; malformed action input can fail the request schema first. Read the current target, review it and submit a new request with updated evidence. Do not change the input under an existing request ID. Exact terminal retries and already satisfied owner actions can still recover without a new record; a legacy pending operation cannot create a new approval without one. `play.entries.stop` closes new enrollments while current journeys continue. `play.pause` pauses the Play, retaining saved progress. Both use the current Play token. Resuming entries or the Play follows the connection's Play checkpoint. `play.run.cancel` takes `playId`, `runId` and the run's `expectedRowVersion`. Cancellation is terminal; a provider send already in flight can still finish, and its receipt is retained. A stopped run is never restarted by that receipt. To reject an unapproved proposal, use `play.discard-proposal` with its exact revision and token. The approved Play is retained. Feedback history links requests to resulting revisions and distinguishes approved, discarded and superseded work. Accepted guidance stays with that Play; rejected proposals do not become standing writing instructions. Operation pages return up to 20 rows and a `next` cursor. Pass it as `after` until `next` is null. Use `status=awaiting_approval` to collect pending reviews without walking completed history. List summaries omit `input`, `result` and `authority`; read the operation by ID when those details are needed. Owner review lists use the same summary shape, with full details at their context endpoint. Each complete operation page is limited to 64 KiB; summary error codes may be shortened to 128 UTF-16 units. Full detail retains the complete error and result. Forwarded-email pages use their returned cursor as `before`. Keep customer identifiers in private request bodies. They must stay out of operation names and URLs, and logs must redact them. Reads are bounded; a request failure is not evidence that a step is incomplete. Use a stable random 1–128 visible-character idempotency key for each logical action. Never use a customer identifier as the key. Exact retries return the original operation. Changed input with the same key returns `request_conflict`; choose a new key for a new decision. After an erasure request is scrubbed, its input fingerprints are gone too. Retrying that key with `person.erase` returns only the original terminal record; it cannot compare the erased input or erase another person. Use a new key for another erasure. A review binds the exact input hash and target version. It also binds the connection policy version. Changing content or policy invalidates old authorization. Read the current state and submit a new operation; do not edit an old request to inherit its approval. Statuses are `awaiting_approval`, `ready`, `running`, `succeeded`, `satisfied`, `failed`, `cancelled` and `stale`. `satisfied` means the exact requested result already exists; it does not create another approval or claim the agent performed someone else's action. Pending operations expire after seven days. External work may finish after a revoke if it had already started; revocation prevents later actions from starting. Cancellation does not undo completed effects. Each workspace can create 100 routine operation records per rolling 24 hours, shared across its connections. Completed, failed and cancelled requests count; exact retries and resumes do not create another record. The separate limit of 100 pending requests per connection still applies. At the daily limit, a needed pause, intake stop or key revocation can still complete. Barua records that stop and performs it in one transaction. Already-stopped or invalid targets cannot use this exception to create more history. Existing history remains readable. Owner endpoints are under `/api/agent-connections`, with an owner session Bearer token and `X-Workspace-Id`. The helper reads `BARUA_OWNER_TOKEN` and `BARUA_WORKSPACE_ID` for these calls: - `GET /` lists connections. `POST /` creates one with `{label,permissions,review,expiresAt}` and `Idempotency-Key`; `expiresAt` may be null. - `PATCH /:id` updates `{permissions,review,expiresAt,expectedVersion}`. `DELETE /:id` revokes the connection. - `GET /operations` lists pending reviews. `GET /operations/:id/context` returns the exact action and its review content. - `POST /operations/:id/review` takes `{decision,inputHash,policyVersion}`. Use `approve` to release the action, or `reject`/`cancel` to close it. Only the owner can widen a grant. An agent operating under an existing management key cannot approve its own required checkpoint or mint another management connection. ## Send one campaign to a saved segment Campaigns use a separate grant from lifecycle plays. Campaign and segment reads require at least one of `segment:write`, `campaign:write` or `campaign:send`. Real-recipient samples and recipient pages also require `people:read`; agents without it receive synthetic language previews. Lists return `{segments,next}`, `{campaigns,next}` or recipient `{revisionId,items,next}`. Keep following `next` as `after` before treating a list as complete. Recipient continuations belong to one prepared audience. If a continuation returns 409 `campaign_snapshot_changed`, reread campaign detail and restart the recipient list; do not combine pages from different revisions. List counts that are absent are not zero; read campaign detail for current results. 1. Read capabilities, setup, `segment-fields` and existing segments. The field response returns `segmentFields` for conditions and `mergeFields` for email personalization. Use their exact paths and types; do not guess paths from an event name or invent fields absent from the approved catalog. `segment.create` takes `{name,definition}`; `definition` has `{version:1,condition}` using the supported customer-fact condition grammar. `segment.update` additionally takes `segmentId` and the numeric `expectedVersion`. Use recorded facts; missing evidence must not become a zero or an invented trait. Reuse a saved segment when it matches the request. 2. Submit `segment.preview` with `{segmentId,expectedVersion}`. Read its returned job ID with `campaign-job`, then read the segment for counts and exclusions. These counts can change before campaign preparation. 3. Submit `campaign.create` with `{name,segmentId,segmentVersion,brief}`. `brief.message` states the email's purpose. Optional `facts` supplies supported details, `cta_url` supplies its destination, `required_fields` names fields every recipient must have, and `optional_fields` maps fields to explicit fallbacks. Use the saved brand, voice and email design. Barua drafts and designs the email; do not handwrite HTML or change workspace brand settings to make a one-off email. `campaign.update` takes those same fields plus `campaignId` and numeric `expectedVersion`; changes require fresh preparation. 4. Submit `campaign.prepare` with `{campaignId,expectedVersion}`. Keep its returned job ID. Read `campaign-job` until it succeeds or reports a problem, then read `campaign`. Do not equate a successful management operation with a finished job. Inspect the fixed audience, exclusion reasons, each language's subject and rendered preview, and any readiness blocker. Barua writes each language variant and uses checked merge fields; it does not generate a new email separately for each recipient. A ready snapshot can have `snapshot.eligible: 0` and no language previews. Report its counts and exclusion reasons; do not request sending or resuming. An otherwise valid send or resume request returns 409 `campaign_empty_audience` before recording an operation. To try a revised audience, use `campaign.update` to create a new campaign revision, then prepare that revision. Calling `campaign.prepare` on the same ready revision returns the saved snapshot without rescanning. 5. Once sending is authorized, submit `campaign.send` with `{campaignId,expectedVersion,revisionId,mode}`. Use the numeric campaign `revision` from detail for `expectedVersion`, and its `snapshot.id` for `revisionId`. The snapshot is the exact prepared content and audience. `mode: "now"` starts at the next allowed opportunity. `mode: "scheduled"` also needs `scheduledAt` as an ISO timestamp with an offset and the intended IANA `timeZone`. `mode: "immediate"` explicitly skips quiet hours and the 20-hour marketing gap. Obtain authorization for that exception; ordinary urgency does not imply it. All modes preserve consent, suppression, unsubscribe, active-send, provider and expiry checks. Optional `expiresAt` is an ISO timestamp with an offset. If omitted, expiry is 14 days after the scheduled start or, for now/immediate, after the approved operation actually starts sending. 6. If `campaign` review applies, report the exact pending operation and wait for owner review. The owner sees the proposed mode, schedule, expiry, audience and language previews. Resume that same operation after approval. A content, audience, version or policy change needs a fresh decision. Newly granting campaign sending enables this checkpoint by default; only an explicit owner choice delegates sending without it. 7. Read campaign detail and, when authorized, every recipient page. The approved audience cannot grow during sending. Consent, suppression and other safety checks can remove recipients. A provider's acceptance is not delivery. Report queued, accepted, delivered, failed, excluded and unknown outcomes accurately. An uncertain provider outcome must be reconciled before another attempt; do not resend it under a new operation. Campaigns do not require enabling lifecycle play sending. Keep workspace configuration unchanged unless the request also authorizes a setup change. To pause or cancel a campaign, submit `campaign.pause` or `campaign.cancel` with `{campaignId,expectedVersion}`. To resume sending, use `campaign.resume` with those fields and the exact `revisionId`; it follows the campaign checkpoint. The helper's plain `resume --id` resumes a management operation, not a campaign. Cancellation leaves provider-accepted mail and its delivery reports intact. Read current campaign detail before choosing an action: | Action | Required campaign state | |---|---| | `campaign.update` | Sending has not started; not completed, cancelled or expired. Creates a new revision that needs preparation. | | `campaign.prepare` | Draft, failed, preparing or ready. Ready returns the existing preparation. | | `campaign.send` | Ready or paused; sending has not started. Requires the exact current prepared revision with at least one eligible recipient. | | `campaign.resume` | Paused, with the exact current revision already approved and at least one eligible recipient in its prepared audience. | | `campaign.pause` | Scheduled, sending or paused. Completed, cancelled and expired campaigns remain unchanged. | | `campaign.cancel` | Any state. Completed, cancelled and expired campaigns remain unchanged. | Expired campaigns cannot be updated, prepared, sent or resumed. These checks apply before a new operation is recorded and again at approval and execution. An older pending request that no longer meets them cannot be approved; the owner can reject or cancel it. Repeating an already completed operation returns its saved result without starting another send. ## Recover a lost management key The owner who created an active connection can use **Save key again** in Settings → Agents. With that owner's already authorized session credentials, the helper can save it directly without exposing the key in output: ```sh node .agents/skills/barua-setup/scripts/manage.mjs recover-key --id "$BARUA_CONNECTION_ID" --secret-file /private/authorized/new-management.key ``` Use a new private destination you are authorized to write. The helper requires `BARUA_OWNER_TOKEN` and `BARUA_WORKSPACE_ID`; it does not require the missing management key. It calls `POST /api/agent-connections/:id/key` with no body or idempotency key, reserving the secret destination first. A retry uses the same connection ID and a new destination. A successful result includes `secret_saved`. Load the saved value into `BARUA_MANAGEMENT_KEY` without printing it, then run `capabilities` to verify usable access. Browser-only tools that cannot securely transfer credentials need the owner to save the key and share only its location. Recovery returns the same key with the connection's current permissions. It does not create another connection or alter pending operations. It requires current terms and the original granting owner to still own the workspace; another owner cannot recover that grant's key. Revoked or expired connections are refused. A changed server seed or failed hash check refuses recovery. Recovery does not rotate a key that may have been exposed. Before diagnosing an older deployment from a missing button, check the original granting owner and active connection, then current terms. If recovery is still unavailable, explain that limitation and arrange a replacement connection with the owner only after securing its handoff. Do not repeatedly ask for an unavailable key. Do not duplicate or revoke existing access without authorization. A revoked or expired grant needs a new authorized connection or owner-approved renewal, not a recovery retry loop. Verify that the deployed recovery path exists before directing the owner there. ## Complete setup and later changes Follow the relevant setup steps, then always complete step 7 before handing back to the owner. This includes a fresh process resuming an approved operation, a partial task and a run waiting at a checkpoint. Full delegation uses the same artifacts and continues without adding a human pause. The package reads and renders; approval and request-changes actions stay in the existing workspace review flow under the current grant. 1. Read capabilities, setup and the named product repos. Complete the [journey review](https://barua.ai/skills/barua-setup/references/journey-review.md) and save its private Markdown/JSON artifacts before proposing or approving events. Review the full experience, including lifecycle, transactional and micro-play opportunities, against the source. Inventory supported variants, recovery transitions and time-dependent conditions before choosing messages. Derive the full event plan and playbook from the reviewed map. Obtain the required independent review through native agent delegation before submitting: the reviewer derives its checklist from product evidence first, then compares the full strategy and exact intended inputs. Record the result and content bindings as described in the journey reference. Self-review is insufficient; if native review is unavailable, hold those submissions without adding a human checkpoint. Review the exact submission's claims, generate the matching Markdown view and pass `--journey-review-file` to plan proposal, plan approval and brand actions. The helper checks the recorded result and matching inputs; it does not prove independent execution or semantic truth. Follow the rendering order in the journey reference. Propose the full plan, including empty arrays when appropriate. Generate the [review package](https://barua.ai/skills/barua-setup/references/review-package.md) for the proposal before requesting approval or approving it under delegation. Approve its exact version or retain the pending operation for owner review. Write emitters at the actual server commit points and validate through the real product flow with the test key. 2. Read brand and its version. Generate the playbook from the reviewed opportunity fields with `journey-review.mjs --render playbook`; keep its exact text in the action input. The helper refuses a separate condensed rewrite. Initial `brand.confirm` includes the exact proposal hash and seen values; later `brand.update` changes the existing brand. Optional `template.import` accepts a workspace-owned forwarded email and checked layout. Preserve unrelated configuration when supplementing an existing workspace. Material changes to the intended strategy or brand patch require another independent comparison. Refreshing a version or checkpoint field requires a current exact action review, without repeating an unchanged semantic review. 3. Provision a sending domain and install the returned records with the already authorized DNS tool. Call `domain.verify` explicitly and read the result. Report absent external permissions precisely; Barua does not store DNS or hosting credentials for this purpose. New setup uses Amazon SES without a provider flag. Use the exact domain in the workspace's From address. Read `GET /domain?provider=ses`; verification also takes its `configId` and `configGeneration`. Install only the returned ownership TXT, DKIM and bounce-subdomain records. Preserve existing Mailgun records and the domain's root MX. Preparation and verification leave the active sender and sending pause unchanged. Activate the verified target explicitly with `domain.activate`, using its `configId`, `configGeneration` and `activeGeneration` as `expectedGeneration`. This needs `launch:write` and follows the optional launch checkpoint. A readiness refusal requires the named prerequisite; never bypass it or treat DNS verification as a completed setup. An explicit `provider: "mailgun"` setup request is refused. `GET /domain?provider=mailgun` can still read historical configurations; Mailgun activation is no longer available. If a legacy setup request returns `provider_retired`, read the SES state and submit a new request. Accepted or uncertain attempts are never resent through another provider. 4. Inspect automatic proposals against the complete journey rules. Shared context in a `per_play` playbook alone does not establish rule coverage. Use the matching reviewed `play.propose` request for a missing supported marketing Play; use `play.request-changes` for an existing Play. Wait for the exact prepared revision. Read each play's current revision and token. Reconcile its rules and renders with the journey before approving selected languages; record exact revision IDs, content hashes and proposed/approved roles, current review tokens and findings in the journey JSON. Inspect each role's policy even if it shares an email revision. Read the canonical `approvalTarget`, record its observation and preview matrix, and pass `--journey-review-file` to `play.approve`; new delegated approvals require the matching evidence. Missing drafts, mismatched behavior and unimplemented prerequisites remain explicit. Request changes or retry authoring when permitted, and verify the result before further approval. A later update follows the same review policy. Existing approved content can continue while a replacement waits; pausing is explicit. An existing workspace in email review can use `review.enable-plays` with the current `expectedGeneration` from `GET /plays/mode`. This follows the play checkpoint and preserves an existing sending pause. 5. Read the latest setup `version` for each launch action. Enable live intake and play sending explicitly. Install the live key and deploy with the authorized hosting tool. Do not clear historical onboarding timestamps. 6. Start `proof.start` with the exact deployed key ID and approved revision ID before triggering a new qualifying action through the deployed product. Bind that event using `proof --bind-stdin` or `--input-file`. The helper retries the same bind while intake is pending. After `event_row_id` is present, plain `proof --id` can poll it. If the command times out or the process stops before binding, repeat the same bind input; a UUID-only read cannot recover an event identifier that has not been stored. The proof follows the actual event, key, revision, enrollment, decision and provider message. Only `delivered` establishes delivery. `provider_accepted` is still waiting for the provider's delivery report. A held event reports the stage and reason code; do not fake consent or bypass suppression to make it pass. Historical setup stamps, a duplicate event or backfill cannot pass this proof. 7. **Finish every run with a current review package.** Follow [Finish and hand off](https://barua.ai/skills/barua-setup/references/management.md#finish-and-hand-off), including when only some of the steps above apply. An earlier process's export does not describe changes made during this run. ## Finish and hand off After the last setup change or resumed operation, run `manage.mjs review-package` into a new private output directory. Do this before returning at a checkpoint too, so the report contains its exact pending operation. Refresh before launch and again after any launch action or delivery check. A successful setup action does not produce or refresh this report automatically. Read the command's returned `artifacts.html` and `artifacts.json`. Inspect the snapshot's plan status, intake and sending state, pending reviews and any proof status before describing the result. Use the current report even if a previous process's files are unavailable. Do not reconstruct old paths or reuse an export from before the operation you just resumed. End with clickable Markdown links using those returned paths: `[Review setup]()` and `[Matching snapshot]()`. Replace the placeholders with the actual paths; code-formatted filenames alone are not links. Include the links in an implementation PR when one exists, using an authorized private location. Link the current private `journey-review.md` and `journey-review.json` too. Refresh their affected rows and exact-revision reconciliation after changes; an older plan approval is not evidence of reviewed journey coverage. Keep files needed by the handoff available. State strategy coverage separately from operational readiness, what remains open and the next permitted action in plain factual language. If collection fails or access is unavailable, state the specific failure and which sections could not be inspected. Do not claim a fresh report exists or silently fall back to a stale one. The report is a dated snapshot, not approval authority or delivery proof. ## Errors and recovery Errors return `{code,error,retryable}`. `invalid_input` requires fixing the body against the current schema. `permission_required` or a revoked/expired grant requires owner action. `terms_required` requires current account acceptance. `target_changed` and `action_changed` require a fresh read and a new decision. `key_slots_full` requires a planned rotation, never automatic eviction. `not_ready` requires the missing setup prerequisite. For `send_in_flight`, retry the same operation after the active send finishes. Use bounded backoff after an uncertain response or a 429/503 error. Keep the same request and idempotency key. `operation_rate_limited` means the workspace's rolling 24-hour creation budget is full. Wait for older requests to leave that window, then retry the same request. Cancelling work does not reset this budget. Necessary emergency stops remain available; repeatedly requesting an already-stopped state does not. A permanently invalid key, revision or job returns the saved operation with `status: "failed"` and an `error_code`; it does not occupy a pending slot. For `proof_input_invalid`, `proof_key_invalid`, `proof_revision_invalid`, `proof_identity_changed`, `key_not_found`, `job_not_found` or `play_revision_invalid`, read the correct target and submit corrected details with a new idempotency key. Exact retries keep the original failed result. `proof_revision_not_ready` instead leaves the operation `ready`: approve or resume that existing revision, then resume the same operation. Unknown database or provider failures remain uncertain and must not be treated as permanent. Domain provisioning allows five admitted attempts per rolling 24 hours. Waiting for another in-flight domain attempt does not spend that allowance. An initial `domain_provision_limit` refusal is saved as `failed`; wait for capacity and submit a new request. An interrupted operation instead remains `ready` with `provider_unconfirmed` and an explanation if the cap is full. Resume that same operation when capacity returns, within its seven-day expiry. Its earlier provider work may have completed even though the response was lost. Event keys remain a separate boundary. New management-created keys carry explicit capabilities. `events:write` admits current events; `history:write` is additionally required for backfill and live occurrences older than 72 hours. An exact already accepted retry stays retryable with its original key and idempotency key. `people:read` and `people:erase` are separate. These restricted keys can read only their own receipts. Existing legacy keys keep their prior capabilities. Revoking an agent connection never revokes deployed event keys; pause intake/sending or revoke the event key separately when that is intended. ## Data retained Barua stores connection hashes and metadata. Operation records include their inputs and outcomes, with the grant owner and policy version behind each action. Pending work expires after seven days. Erasure-request identifiers and their input fingerprints are removed when the action completes or is cancelled. Expiry and the person's erasure also remove them; bounded maintenance can lag during an outage. Connections, including revoked or expired ones, and their action metadata remain for audit and idempotency until the granting account or workspace is deleted. Immutable play approvals keep the agent attribution even after that connection is removed. Delivery proof links follow deletion of their underlying event. Secrets do not belong in operation input or free-text labels. --- Source: https://barua.ai/skills/barua-setup/references/owner.md # Owner access without the app Use `node /scripts/owner.mjs ` for an owner's account, workspaces and exact decisions. Install `owner-storage.mjs` and `private-input.mjs` beside it. These files use Node built-ins only, on Node 18.17 or newer. Routine agent work uses the separate [management connection](https://barua.ai/skills/barua-setup/references/management.md). ## Sign in privately Set `BARUA_URL` to the trusted barua origin, or use its default `https://barua.ai`. HTTPS is required except on localhost. Create a directory owned by you with mode 0700 outside the repository. Set `BARUA_OWNER_SESSION_FILE` to a file in it. `--session-file` can also name it. The helper writes that file with mode 0600, serializes refresh and selection, and replaces it atomically. Symlinks, hard links and files readable by other users are refused. Inside Git, ignore both the file and its `.lock` directory. Do not remove a lock while another command is using the session. `login` takes private JSON stdin or `--input-file` containing `email` and `password`. Passwords and tokens never belong in arguments. Output contains the account identity and selected workspace, with credentials redacted. `session` reads local sign-in status; `refresh` refreshes it; `logout` revokes this session only. A failed logout keeps local credentials so you can retry. For existing callers, `BARUA_OWNER_TOKEN` and `BARUA_WORKSPACE_ID` authorize owner API commands. Do not configure both an environment token and a saved session: the helper refuses ambiguous credentials. Login, refresh, logout and password changes need the saved session. Account deletion also accepts an owner token. The public `/api/auth/config` supplies the fixed auth issuer and publishable key; service credentials are never requested or used. ## Admission, workspaces and terms Run `terms` to read the exact current legal version, digest and document paths. Read those documents before accepting. Never insert an acceptance automatically because setup needs one. `signup` remains restricted by early-access admission. Its private JSON takes `email`, `password`, `invite` when required, `site_url`, optional `from_name`, `accepted_terms: true`, and your exact `accepted_version` and `accepted_digest`. It requires a new `--secret-file` or authorized `--secret-command-file` for the pixel key. The helper saves the key before saving its signed-in session. `workspaces` lists memberships; use `--after` with its `next_cursor` to keep reading. `workspace-create` takes the workspace fields and exact acceptance, plus a key destination. `workspace-select --workspace ` verifies current membership before storing that selection. Scoped commands cannot silently fall back to another workspace. `me` reads its role and terms state. `terms-accept` takes `{"version":"the reviewed version","digest":"the reviewed digest"}`. Signup and workspace creation are not automatically retried. After a lost response, sign in and inspect `workspaces` before creating again. An admitted account survives interrupted setup. Do not delete it to recover setup. If the pixel-key response was lost, `pixel-key-rotate` replaces that workspace's key through the existing API. It requires a new `--secret-file` or authorized `--secret-command-file`. The previous pixel key stops working immediately. Update any existing installation explicitly; never rotate automatically after an uncertain response. Management and integration keys are separate. ## Recovery and team `recovery-request` takes private JSON with `email`. Read the email, then give `recovery-complete` private JSON with `recovery_url` and the new `password`. The original one-use link must come from the configured sign-in issuer and have `type=recovery`. `token_hash` may replace `recovery_url`. Do not open the link first or paste it into arguments. The helper exchanges it, saves the session, then updates the password. If the last step fails, `password-update` with private `{"password":"..."}` finishes using the saved session. If the verification response itself is lost, request a fresh recovery email. If a saved session already exists, use a new private session file or log out before recovery. A recovery link never replaces another saved account. `team` lists members and invitations. Follow `members_next_cursor` through `--members-after`, and `invitations_next_cursor` through `--invitations-after`. `member-role --id` takes `{"role":"owner"}` or `{"role":"member"}`. `member-remove --id` removes a member or leaves when the ID is your own. The server always preserves the last owner. `invite` takes an email address in private JSON. `invite-resend --id` and `invite-revoke --id` act on an existing invitation. An `unknown` or `failed` delivery status exits 5. No invitation send is automatically retried. After an unknown result or lost response, read `team` before deciding to resend. Resending changes the link; the server's cooldown applies. Recipients use `invitation-preview` with private `{"token":"..."}`. `invitation-register` takes the token and a password for a new account; existing accounts sign in. `invitation-accept` takes `token`, `accepted_terms: true`, `accepted_version` and `accepted_digest`. Select its returned workspace ID. Joining does not admit an account to create unrelated workspaces. ## Grants, reviews and email controls `connections` reads grants. `connect` takes `label`, `permissions`, `review` and optional `expiresAt`, plus a stable `--idempotency-key` and secret destination. Reuse the same key and exact body after response loss. `recover-key --id` saves the existing connection key again for its granting owner. `connection-update --id` takes `permissions`, `review`, `expiresAt` and the current `expectedVersion`. `connection-revoke --id` revokes the grant. A secret file must be new and, inside Git, ignored. A secret-command file is a JSON argv array for a previously authorized command. It receives the key on stdin with output suppressed, no shell and no inherited Barua credentials. After a transfer failure, recover the same key into a new destination. `reviews` lists pending requests, with `--after` pagination. `review-context --id` reads the exact request and current policy. `review --id` takes `decision` (`approve`, `reject` or `cancel`), `inputHash` and `policyVersion`. Review does not resume the operation. Its agent must still resume it through the management helper. Existing email controls are `drafts --status`, `draft --id`, `autonomy`, `autonomy-plays`, `draft-approve --id`, `draft-reject --id` and `autosend`. Draft decisions take `{}` or an optional `note`. Approval can send an email. Autosend takes `{"enabled":true}` or `{"enabled":false}`. Play and campaign restrictions remain enforced by the server. Use their management workflows for those decisions. These writes are never automatically retried. ## Close a workspace and keep its export Workspace closure and account deletion are separate decisions. Closing a workspace stops its work and queues export and cleanup. It does not delete the owner's sign-in account or their other workspaces. Do not close a production workspace, request erasure, extend retention or delete an account automatically as part of setup, testing or recovery. These commands require the owner's explicit decision for that exact target. Read `closure-status --workspace ` first. To request closure, send private JSON to `workspace-close --workspace `: ```json { "request_id": "a-new-uuid-saved-for-this-request", "confirm_workspace": "the-exact-workspace-uuid", "confirmation_version": "2026-10-23-v1" } ``` Supply real UUIDs and save that request. The helper does not fill in confirmation or retry closure automatically. After a lost response, read `closure-status` before explicitly retrying the same request. A queued closure is not a finished export. Report its phase, blockers and download availability from the response. Primary data cleanup waits for the verified export; membership remains while export, retention or erasure obligations are outstanding. The archive expires 30 days after closure is requested by default. Read the exact `archive_expires_at` from status. An explicit owner decision can extend it with `closure-extend --workspace ` and private JSON `{"until":"the-chosen-ISO-timestamp-with-time-zone"}`. An extension must move the deadline forward and cannot restore an archive already being removed. There is no automatic extension, including when a slow export reaches its deadline. Read the blocker and obtain an explicit retention decision. Use `closure-artifacts --workspace ` for a bounded manifest page. The server defaults to 20 records; `--limit` accepts 1 through 100. Continue with `--after ` from the page. Download each required chunk with: `closure-download --workspace --id --revision --output-file `. Keep the revision exactly as returned, including large decimal values. Each chunk is at most 1 MiB. The API verifies its checksum, byte count and current authority before releasing it. The helper uses the authenticated API directly, refuses redirects and never creates or follows signed download URLs. It writes the original bytes to a new mode 0600 file inside a private 0700 directory. Existing files and symlinks are refused; failed downloads remove their new file. Output contains only its destination, byte count and checksum. Keep exports out of logs, shared directories and Git. For an authorized person's erasure, `closure-erase --workspace ` takes private JSON `{"email":"the-persons-address"}`. Never put the address in an argument or URL. Downloads are held until redaction has been verified; the request response is not proof that erasure is finished. Read status and the current manifest again. Old revisions cannot bypass the hold. After final archive removal, closure status remains available to recorded owners while their account exists, but no archive bytes remain available. ## Delete the sign-in account separately `account-deletion` reads the current account ID, deletion version and blocking memberships. Any workspace membership blocks deletion, including an archived workspace with unfinished obligations. Leave or finish closing each workspace first; the last owner cannot be removed while their obligations remain. 1. Review the exact account ID and version `account-deletion@1`. Run `account-delete-prepare --receipt-file --idempotency-key ` with private JSON `{"account_id":"the-exact-account-uuid","version":"account-deletion@1"}`. The helper saves the request before sending it, then durably saves the returned receipt before reporting success. Preparation alone deletes nothing. 2. Keep the receipt file. It is mode 0600 in a private 0700 directory, bound to the original barua origin. The response gives the account, version, request ID and preparation expiry. Preparing expires after 15 minutes; a same-request retry while signed in returns and saves a replacement receipt. 3. Only after the owner's deletion decision, run `account-delete-confirm --receipt-file ` with private JSON `{"account_id":"the-exact-account-uuid","version":"account-deletion@1","request_id":"the-saved-request-uuid"}`. All fields must match the saved request. The helper reads the receipt from that file; a pasted receipt alone cannot authorize deletion. Confirmation also requires the account's verified owner JWT. 4. After a lost confirmation response, run `account-delete-status --receipt-file `. It loads no sign-in session and sends only the private receipt. It can report completion after sign-in stops working. This command is status-only: it cannot delete, recreate an account, fetch personal data or download an archive. Follow its safe next action; another confirmation still requires the original account's JWT. After a lost preparation response, retry with the same file, account, version and request label while signed in. If the file or request label itself was lost, wait until the unconfirmed preparation expires, then prepare with a new label and a new private file while signed in. The server assigns a new request ID and invalidates all previous receipts. Confirmed deletion can never be restarted this way. Never discard a receipt because a command failed. Receipts and credentials stay out of arguments, URLs, output and logs. Within Git, ignore the private file and its `.lock` directory. A successful confirmation clears matching credentials from the helper's saved session. An externally supplied token remains the operator's responsibility. Legal acceptance evidence is retained for five years under its account or workspace policy. Deletion does not remove that evidence immediately. Completed closure receipts lose the deleted account's identifier; other owners retain their own status access. Run `--help` for options and exit codes. Describe a completed owner decision separately from a resumed agent operation, and provider acceptance separately from delivery. --- Source: https://barua.ai/skills/barua-setup/references/reading-repos.md # Reading the product's repos Read every repo the owner names. A monorepo counts once, with several roles. First understand the complete customer journey and its outcomes, then locate the server commit point for each supporting fact. Existing emitters are evidence of implementation, not a boundary on the intended journey. Source files and the full discovery inventory stay on the machine. Barua gets the declared events and configuration you submit, including the source-backed playbook. Management Play approvals also send the artifact digest with compact review findings and the exact policy and preview references. ## Journey discovery before event selection Follow [journey-review.md](https://barua.ai/skills/barua-setup/references/journey-review.md) before proposing or approving a plan. Trace routes through server handlers, persisted state, workers and existing emails. Include alternate creation modes, incomplete actions, failures, recovery, payment states, fulfilment, returning users and account controls where the product supports them. Look beyond the successful signup-to-purchase path and beyond files that already mention Barua. Make two recorded passes. First inventory the source-owned entities and their variants. Follow state changes through failure and recovery, then record time conditions and current senders. Keep that list separate from the messages you want to propose. Use the `source_inventory` fields in the journey reference. Before selecting events, complete its [fact-to-decision checks](https://barua.ai/skills/barua-setup/references/journey-review.md#trace-facts-through-every-decision) for shared consumers, preexisting state, action prerequisites and time without callbacks. Next map every material source row to an assessed opportunity or control decision. Check the exact plan and playbook you will submit against those facts. Distinguish observations from proposals; leave unknown promises out of the submission. A retry endpoint does not establish that retry is free or available to the customer. Follow its authorization and charge behavior. Record sending ownership and duplicate prevention when another system keeps sending a message. Its event may still stop a different Play. Missing emitters remain implementation work; unresolved product facts remain dependencies. ## Frontend - Find the journeys: routes, forms and buttons that change server state. - Trace each action to the request it makes, and that request to the backend handler that answers it. The handler is where you emit. - In Next.js, files marked `"use server"` and `app/**/route.ts` are server code. Files marked `"use client"` are browser code. - Client analytics calls (PostHog, Segment, Mixpanel, gtag) hint at which facts matter. They are never emit sites. - Record an action that makes no request when you find it. Pass local JSON to `node /scripts/handoffs.mjs --record ` through stdin, using the private receipt file from setup: ```json {"needs_server_call":[{"source":"web/editor/Publish.tsx:onPublish","action":"Only changes local state.","endpoint":"POST /api/posts/:id/publish"}]} ``` Use the actual source and behavior; this is an example, not an event to add. The helper keeps the gap through later API responses and includes its `needs_server_call` line in the final report. The endpoint is a suggestion for the owner, not permission to create it. Never emit from the browser to cover the gap. These local notes are not sent to Barua. ## Backend - Find the commit point: the line after the transaction or ORM write resolves. Look for Prisma `$transaction`, Drizzle or Knex transactions, Django `transaction.on_commit` and Rails `after_commit`. - Send from code that is still running when the send finishes. A serverless or edge platform can freeze or end the invocation once the response goes out. A send still in flight then dies with its retries and its failure log, so Barua never gets the event and nothing records the loss. Use the first of these the product has: 1. A durable job queue or an outbox table, including one you add. Write the event in the same transaction as the fact, then return the product response when that transaction commits. The request must not await the worker, a flush or a drain, even after the commit. The independently scheduled worker calls and awaits `emit` outside any database transaction, then records the result in a short transaction. A slow delivery must not hold a database lock needed by product requests. When the result isn't `ok` and its `status` is 429 or 5xx, or its `body.code` is `network_error` (a timeout or a network failure, so no status came back), fail the job so the queue retries it. Keep the saved job until its result is recorded; the worker must recover it after a restart. Barua drops a resend with the same `event_id`. 2. The platform's `waitUntil`, which keeps the invocation running after the response: `waitUntil` from `@vercel/functions` on Vercel (`after` from `next/server` in Next.js 15.1 and later), `context.waitUntil` in Netlify Functions and Edge Functions, and `ctx.waitUntil` in Cloudflare Workers. Cloudflare stops that work 30 seconds after the response. Vercel and Netlify stop it at the function's own time limit. 3. Only when neither a durable queue/outbox nor `waitUntil` is available, `await` the send before you respond. The response waits for Barua. Adding an outbox selects the first way; do not combine it with this fallback. - Never start a send that nothing waits for, such as `void emit(...)`, or an `emit` call nobody awaits and no `waitUntil` holds. - Fit `attempts` to the time the send gets. It takes a whole number from 1 to 5, and `createBaruaEmitter` throws on any other value, so a bad setting fails at startup. Each attempt waits at most 10 seconds for Barua, and each pause before a retry lasts at most 30 seconds, so the default 5 attempts can take up to 170 seconds. `createBaruaEmitter({ ..., attempts: 1 })` takes at most 10 seconds and never retries. Use it under Cloudflare's `waitUntil` and for the no-queue request fallback. A queue worker can also use one attempt and let its saved job handle retries; that does not make the product request wait for the worker. A send that fails without a saved job is lost unless something sends it again, which is why a queue comes first. - `emit` never throws, so read its result wherever it runs and log a failure with the event's name and `event_id`: ```ts const result = await emit(event); if (!result.ok) console.error('Barua event not sent', event.name, event.event_id, result.status, result.body); ``` Under `waitUntil`, hand it the send with the same check: ```ts waitUntil(emit(event).then((result) => { if (!result.ok) console.error('Barua event not sent', event.name, event.event_id, result.status, result.body); })); ``` - `signed_up` comes from the line that commits the user row. Its `event_id` is `signed_up:`, and its `external_id` is that same id. Send the user's email as `properties.email`. - `marketing_opt_in` on `signed_up` comes from the signup form's consent checkbox. A later preference change is a `consent_updated` event. Send one that turns marketing off only from a job queue or an outbox, the first way in the list above, because a lost opt-out leaves Barua treating someone who said no as opted in. - `activated` is the first moment the user gets the product's core value, such as a first published project. It's your choice: name it in the report so the owner can change it. - `created` is one event per row of the main thing the product exists for. The `Use` column of the property table in `references/events.md` says which of its properties may appear in an email. - Payments: `purchased` and `churned` usually come from the payment provider's webhooks. See [Payments](https://barua.ai/skills/barua-setup/references/reading-repos.md#payments). The trial and subscription events and `order_refunded` come from there too, and the order status events from the shipping system. - Account deletion: read the user's email before the delete, and in the same transaction write an erase job that keeps the user's id and that email, as a row in an outbox table or in a job queue that commits with that transaction. That's the first way in the list above, and the only one an erase may use. The job calls `POST /api/integrations/v1/people/erase` twice, with the user's id as `external_id` and then with that `email`, because the address Barua holds for the id can be an old one when the email changed, and it retries each call until it answers 200 with `"erased": true`. It needs the live key, so it runs only in production. `references/events.md` has the bodies and what the job does with each answer. It also covers a delete your database doesn't run, and a product with no job queue. - A product fact the core events don't cover (a team invite, a report export) becomes a custom event in the plan, with each property marked `copy` or `targeting`. Check the core list in `references/events.md` first: a trial, a subscription, a shipment and a sign-in are core events now. ## Payments `purchased` means the money arrived. Send it when the payment provider confirms the payment, and never while a payment is still processing. Each payment sends one `purchased`, from one handler. - Stripe Checkout, one-time payments (`mode` is `payment`): on `checkout.session.completed`, send `purchased` only when the session's `payment_status` is `paid`. A delayed method, such as a bank debit, completes the session with `payment_status` `unpaid` and settles later. Its `purchased` waits for `checkout.session.async_payment_succeeded`, and `checkout.session.async_payment_failed` sends nothing. Use the session id as `order_id` in both handlers. - Stripe invoices: `invoice.paid` sends `purchased` for each paid invoice, with the invoice id as `order_id`. It fires for every subscription payment, the first one included, and for one-off invoices. Skip an invoice whose `amount_paid` is 0, such as the one a trial starts with. In `subscription` mode, leave the Checkout session out, because its payment is that first invoice. In `payment` mode with post-purchase invoices on, the session also makes an invoice for the same payment (the session's `invoice`). Skip that invoice here, because the session already sent the purchase. - Use `payment_intent.succeeded` only when the product charges through PaymentIntents directly. Checkout sessions and invoices create their own PaymentIntents, so a product that uses either would count each of those payments twice. - Stripe amounts (`amount_total` on a session, `amount_paid` on an invoice) are integers in Stripe's charge unit, which isn't always the currency's ISO 4217 minor unit. Stripe still writes ISK and UGX with two trailing zeros, so 500 means 5 ISK. Barua's `amount` is in major units and its `currency` is the ISO 4217 code in capitals. Read the amount together with the object's `currency` and convert with this table. It holds Stripe's lists as of 2026-10-09 (https://docs.stripe.com/currencies): ```ts const STRIPE_ZERO_DECIMAL = new Set(['bif', 'clp', 'djf', 'gnf', 'jpy', 'kmf', 'krw', 'mga', 'pyg', 'rwf', 'vnd', 'vuv', 'xaf', 'xof', 'xpf']); const STRIPE_THREE_DECIMAL = new Set(['bhd', 'jod', 'kwd', 'omr', 'tnd']); // Every other code, ISK, UGX, HUF and TWD included, has two decimals in Stripe's amounts. function stripeAmount(amount: number, currency: string): { amount: number; currency: string } { const code = currency.toLowerCase(); const divisor = STRIPE_ZERO_DECIMAL.has(code) ? 1 : STRIPE_THREE_DECIMAL.has(code) ? 1000 : 100; return { amount: amount / divisor, currency: code.toUpperCase() }; } ``` A webhook Stripe delivers twice builds the same `event_id`, so Barua keeps one. - `customer.subscription.deleted` gives `churned`. `references/events.md` maps Stripe's other subscription and refund webhooks. - A refund never sends `purchased`, and neither does a failed or disputed payment. A refund sends `order_refunded`. - Every other provider, PayPal and Shopify included, follows the same rule. Send `purchased` on the signal that says the money arrived, such as a captured or paid payment, and never for an order that is only authorized or still pending. An app store purchase counts once the store confirms it. An invoice your product marks paid by hand counts once it's marked. - For any other provider, read its docs for the unit of the amount it sends before you convert. Barua wants major units, such as 19.99. ## Database and migrations - Read the DDL for each property's type and length, and for the row timestamp that becomes `occurred_at`. - For history, write read-only `SELECT`s to `barua/backfill/.sql` that return rows in the event's shape. Never run them against production. Whoever sends their rows sends them with `"mode": "backfill"`, `signed_up` first, so the live deploy check waits for the product's own live events. --- Source: https://barua.ai/skills/barua-setup/references/review-package.md # A reviewable setup proposal Give the owner a readable proposal alongside the implementation PR and final handoff. They should be able to judge which product facts become events, who receives each play, and what the proposed emails say. First complete [journey-review.md](https://barua.ai/skills/barua-setup/references/journey-review.md), before proposing the event plan. Its private Markdown and JSON files record the intended experience, including missing implementation, external senders and deliberate exclusions. Keep them beside this report in the handoff. The exported package describes stored configuration; it cannot establish that the strategy covers the product. Do not put the journey JSON into `--input-file`: that option has the separate strict source-evidence schema below. ## Default path With `BARUA_MANAGEMENT_KEY` loaded in the helper's environment, run: ```sh node .agents/skills/barua-setup/scripts/manage.mjs review-package \ --output-dir /private/authorized/barua-review-001 \ --input-file evidence.json \ --timeout-ms 300000 ``` Use your installed skill path and an authorized private directory. Its parent must exist; the output directory must not. The whole skill must be installed, including `scripts/review-package.mjs` beside `manage.mjs`. The command needs Node 18.17 or newer and no package dependencies. It reads the real workspace selected by the management key, including when event testing uses a test key. `--input-file` is optional. Without it, the package states that source implementation evidence was not supplied. Add `--proof-id ` to read an existing delivery proof. This does not start or bind one. The shared helper timeout defaults to 60 seconds; the example uses its 300-second maximum. The report shows the selected proof's status and timestamps without claiming freshness, even when its email revision still matches an approval. Reading this record does not verify current launch settings or the event key. It does not recheck the email provider. Generate a package once the plan is proposed, before asking for a required checkpoint or approving under delegation. If an action returns `awaiting_approval`, refresh into a new directory so the handoff includes that operation. Refresh after changing the plan or plays, before launch, and after the delivery check. Complete the management procedure's [Finish and hand off](https://barua.ai/skills/barua-setup/references/management.md#finish-and-hand-off) step after the last change in every run, including checkpoint and continuation runs. Every handoff links the current artifact and names what still needs a decision. Full delegation follows the same artifact workflow and continues through permitted actions without a human pause. ## What the owner receives `index.html` is the readable report; `review.json` is its matching snapshot. Successful command output is JSON with `ok`, `exit: 0`, `package_id`, `captured_at`, `artifacts.html` and `artifacts.json`, plus counts for `plays`, `previews` and `pending_reviews`. Link the returned artifact paths. The report includes: - Core and custom event contracts, property constraints and omissions, with any source implementation evidence supplied by the agent. A changed custom event shows both its proposed and approved contracts, even when its name stays the same. Removed events retain their approved contract. - Proposed and approved play versions, their audience rules and timing, exclusions, inherited play history, personalization and any supplied rationale. Service emails use the current review screen's purpose wording; exact details and JSON retain the saved policy and brief unchanged. - Synthetic example emails for each available step and language in those revisions. Missing or failed authoring is explicit. Remote images are blocked in the exported previews. - Workspace language selection, supported and fallback languages, marketing send hours and weekday rules, and the timezone fallback order. - Every captured event status, including unknown and unnamed events, with counts, latest results and available reason/fix diagnostics. Management supplies reduced test/live verdicts; absent detailed invalid, missing and truncation lists are not evidence of a clean result. A failed verdict carries that limitation and requires a current event-status check before launch. - Current launch facts and pending review operations. Include an existing delivery proof when requested. Historical setup stamps do not establish fresh delivery. Record versions as well as names. The package captures its workspace and time, connection policy, workspace version, plan version, play revision IDs and content hashes. A replacement waiting for approval does not imply that the prior approved play stopped sending. Inspect each version's actual state. Before approving a play, use these exact rules and previews to reconcile it with the journey opportunity. Save the revision ID, content hash, proposed or approved role, current review token and observed findings in the journey JSON. Shared email revisions can still have different policies; check each role's rules separately. A readable report is not a passing coverage or play review. Missing drafts and mismatches remain unfinished work. Link the HTML and JSON in the final handoff and implementation PR using the authorized private artifact location. A local file link works only for someone with access to that filesystem; say so if the reviewer needs an authorized private transfer. Do not publish the package or commit it by default. ## Optional source evidence Use plain factual sentences. Explain the real trigger and why it helps the recipient. Name omissions and assumptions directly. Avoid promotional claims, dramatic fragments and contrast slogans. Never invent a test result or infer deployment from an emitter's presence. The input is a strict JSON object of at most 512 KiB. All top-level fields below are required when an evidence file is supplied; arrays may be empty: | Field | Meaning | | --- | --- | | `workspaceId` | The current workspace ID | | `planVersion` | Proposed plan version when present, otherwise the approved version | | `sourceCommit` | Inspected Git commit, 7 to 64 hexadecimal characters | | `summary` | A short explanation of the intended setup | | `events` | Up to 100 entries with `name`, `source`, `trigger`, `why`, `status`, and optional `checks` | | `plays` | Up to 200 entries with `playId`, exact `revisionId`, and `why` | | `assumptions` | Up to 50 statements the owner may need to confirm | | `remaining` | Up to 50 statements of work that is still outstanding | An event's `status` is one of `planned`, `implemented`, `tested`, `deployed` or `not_applicable`. Each event may have up to 20 checks, each with `command` and `result` strings. Free-text values must contain 1 to 4000 characters. Event names must be unique and belong to the selected plan. Play/revision pairs must be unique and match the revisions in the package. Unknown fields and stale workspace, plan or revision identities are refused. Example shape, with IDs and version replaced by the workspace's current values: ```json { "workspaceId": "00000000-0000-4000-8000-000000000001", "planVersion": 2, "sourceCommit": "0123456789abcdef0123456789abcdef01234567", "summary": "Help new accounts finish their first published project.", "events": [{ "name": "activated", "source": "src/projects/publish.ts:publishProject", "trigger": "The first project publication commits successfully.", "why": "Stop setup reminders once the account has published.", "status": "implemented", "checks": [] }], "plays": [], "assumptions": ["The first published project is the activation milestone."], "remaining": ["Run the publication flow with a test event key."] } ``` These notes are agent-reported evidence, separate from facts read from Barua. A status of `deployed` in this file is not API-observed proof. Keep secrets and customer data out of the notes. Include file locations, not source code. ## Decisions and changes The package is a snapshot for review. It never approves, resumes, sends, changes policy or performs customer lookup. Its preview POST requests render synthetic examples only. No `people:read` permission is needed for those examples. Use the report's workspace review links to inspect the current version in Barua. The existing review flow binds decisions to the exact operation input, target version and connection policy. A stale export cannot approve a newer proposal. After changes, collect a new package and submit a new operation when the target changed. Retain the old package as a dated record. When requesting changes, name the event plan version or play revision and the specific change wanted. Use the existing Plays review screen or, within the agent's current grant, `play.request-changes` with that revision's current token. Read the resulting detail and generate the replacement package before review. Plan edits use a new full proposal. Never treat comments in an artifact as an approval receipt. ## Event-only access With only `BARUA_API_KEY`, complete the same journey review before submitting the plan, then create a private `event-plan.md` using the integration API's plan contract and the repo evidence already gathered by the setup skill. Cover each relevant core and custom event: meaning, exact server trigger, properties, rationale, implementation/check status, and omissions or assumptions. Name the current proposed or approved plan version and retain every returned `needs_human` step ID with its URL and reason. State that proposed plays, their email previews and management launch facts are unreadable with this access. Report any integration setup facts you can read with their actual scope. Continue authorized emitter work. Do not request a management key solely to create this artifact, and do not invent previews. Link the intended journey Markdown/JSON beside the event proposal; mark actual play reconciliation unavailable with this access. ## Errors and privacy Collection failures write no successful partial package. Fix the reported failure and rerun into a new directory. Before saving, the helper rechecks every page of pending requests and Play summaries, alongside the workspace data already captured. A selected delivery proof is also read again. A changed captured value stops the export before any files are saved. Both passes share the command's time and size limits. These reads detect observed changes during collection; activity after the final reads can still make a saved package stale. | Code | Recovery | | --- | --- | | `unexpected_response` | Check the evidence schema and its current workspace/plan/revision bindings, or the API response named by the error. | | `snapshot_changed` | Refresh the proposal and evidence, then collect again. | | `package_limit` | Collection exceeded 100 plays, 256 previews, 100 pages per list pass or 16 MiB. Report the limit; do not silently omit content or change workspace data to fit it. | | `timeout` | Retry with `--timeout-ms 300000`; report a continuing timeout. | | `server_update_required` | The management API must deploy before this skill is published. Canonical core definitions are unavailable. | | `output_exists` | Choose a new private output directory. | | `missing_module` | Reinstall the whole skill, including the sibling report module. | Files are created privately: mode 0600 inside a new mode 0700 directory on systems that enforce those modes. An existing output directory is refused; no report is overwritten. Windows permissions need an appropriately private destination. Artifacts may contain business plans and email copy even though credentials are redacted. See [management.md](https://barua.ai/skills/barua-setup/references/management.md) for permissions and action recovery, and [events.md](https://barua.ai/skills/barua-setup/references/events.md) for the event contract.