Skip to main content
Brief Cascade well and it will wire up email correctly, then you brief it again from scratch on the next project. Windsurf’s Rules and Workflows files turn that briefing into something you replay instead.

Prerequisites

Cascade writes and runs code, but it can’t verify your domain or create your credentials. Do these in the AhaSend dashboard first.
1

Verify a Sending Domain

Add the SPF, DKIM, and DMARC records from the domain setup guide, then confirm the domain is verified before sending.
2

Create an API Key

Follow the API credentials guide and use a domain scope such as messages:send:{example.com} instead of messages:send:all: if it leaks, the damage is limited to one domain’s outbound mail. Domain-specific scopes are written with the curly braces around the domain. Grab your account ID too.
3

Put Both in Your Environment

For local agent-run tests, create a dedicated, domain-scoped credential in Sandbox mode. Set its AHASEND_API_KEY, AHASEND_ACCOUNT_ID, and AHASEND_DELIVERY_MODE=sandbox in .env, and keep that file out of git. Configure live credentials in your deployment’s secret manager rather than giving them to an agent-run test.

Step 1: Install the SDK Before You Prompt

Do this yourself. An agent that finds the dependency already in package.json has far less room to improvise a different one.

Step 2: Give Cascade the Ground Truth with a Rules File

Give Cascade an authoritative project Rule instead of relying on generated code to reconstruct the integration from general context. Cascade can also search documentation, but a Rule keeps the critical constraints available inside the repository. Workspace Rules are Markdown files with an activation mode, discovered under .devin/rules/ in your workspace — the preferred location in current Windsurf builds, with the older .windsurf/rules/ still read as a fallback. Create a model-decision rule so Cascade loads the full instructions when its description matches an email task:
.devin/rules/ahasend-email.md
To start from a draft, point Cascade at AhaSend’s machine-readable doc index at https://ahasend.com/docs/llms.txt, ask it to write the rule from those pages, then check the result against the list above.

Step 3: Capture the Integration as a Workflow

A Workflow is a Markdown file under .windsurf/workflows/ with a short frontmatter description and a series of steps: a saved prompt-plus-procedure that Cascade can replay instead of you re-explaining the task each time. Workflows are manual and specific to Cascade. Open a Cascade session and invoke this one with /ahasend-email; another agent mode will not run it, and Cascade will not select it automatically.
.windsurf/workflows/ahasend-email.md
Adding email to a new screen is now one invocation, and because the recipe encodes the same facts as your Rules file, every replay lands the integration the same way.

Step 4: Prompt Cascade with Specifics

Whether you’re invoking the Workflow or asking directly, name the file and the behavior:
  • Weak: “add email to my app”
  • Better: “In the signup handler, after the user is created, send a welcome email through AhaSend using /ahasend-email. Reuse the client from lib/ahasend.ts, use the server-side signup event ID for idempotency, and await the send. If response latency matters, enqueue a durable job and send from its worker.”
Here is the code the Workflow is grounded against:
lib/ahasend.ts
routes/signup.ts

Step 5: Test in Sandbox Mode

Cascade can run terminal commands, but a production-capable API key remains production-capable even when one request sets sandbox: true. Prefer mocked tests. If you explicitly approve a live integration check, use a dedicated domain-scoped credential configured in Sandbox mode. Sandbox mode validates and processes the message, triggers relevant configured webhooks, consumes no email credits, and never delivers it.
With explicit approval, ask Cascade to send once with the dedicated Sandbox-mode credential and confirm no recipient result has status === "error". Swap sandbox_result to "deliver", "bounce", "defer", "fail", or "suppress" to trigger the applicable configured webhook event. A webhook test also needs a configured endpoint that AhaSend can reach. Keep delivery fail-closed through AHASEND_DELIVERY_MODE; do not infer live delivery from a generic environment name.

Step 6: Read the Diff

Confirm these before you commit:
  • The real package. The import must be @ahasend/sdk. If you see another package name, or raw fetch calls in a plain Node service, a guess slipped past the rule.
  • No open relay. If Cascade added an HTTP route that sends, check that the recipient comes from the authenticated session or a server-side record rather than the request body, that the route is authenticated and rate-limited, and that the body size is bounded. A handler taking to, subject, and html from an unauthenticated caller is a spam relay on your verified domain.
  • Every recipient checked. A promise that resolves means the request was accepted, not that every address was. Code reading result.data[0] and moving on will silently miss a suppressed recipient in a batch.
  • The key comes from the environment. An agent editing several files can drift a secret into the wrong one. Confirm the key is read from process.env and never written to anything git tracks.
If Cascade scaffolds frontend code, make sure the key never lands in a client-exposed variable (NEXT_PUBLIC_, VITE_, PUBLIC_, NUXT_PUBLIC_). Anything with those prefixes ships to the browser.

Going Further

  • Write a second workflow for the failure path: one that walks sandbox_result through bounce and suppress, so rehearsing a rejection is a slash command rather than a memory exercise.
  • Templating and webhooks: per-recipient substitutions cover personalised batches, and @ahasend/sdk/webhooks verifies delivery-event signatures. Add both to your rules file so Cascade reaches for them unprompted.
  • Same grounding trick, different file: Cursor reads .cursor/rules/, Claude reads CLAUDE.md, and GitHub Copilot reads .github/copilot-instructions.md.
  • For the finished shape of what Cascade is building, see Express, Fastify, or SvelteKit.

Troubleshooting

Confirm the file is under .devin/rules/ in the current workspace and has valid trigger frontmatter. With model_decision, Cascade sees the description first and loads the rule when it decides that description is relevant. Make that description more specific, or change the trigger to always_on if the full rule must be included on every request.
Workflow steps are instructions, not a script. Make the last step an explicit, checkable assertion (“confirm no entry in result.data has status error before reporting done”) rather than a vague “test it”.
If Cascade put the client in frontend code, move the send to a server-side route. If it is already server-side, verify the required environment variables and account ID. The browser check is defense in depth, not a substitute for keeping credentials out of client bundles.
The from address must belong to a verified sending domain on your account. Check domain status in the dashboard, and see the quickstart if you haven’t added one yet.
Every endpoint the SDK exposes is in the API reference.