Windsurf
Configure Windsurf Cascade for Anlyon development: governed actions, approvals and webhook verification
Windsurf’s Cascade assistant writes code from your project context. This guide shows how to give it the rules for an Anlyon integration, so the code it writes routes production calls through actions and never carries a credential.
Prerequisites
- Windsurf editor installed
- Node.js 18+ installed
- Two Anlyon API keys from your dashboard: an operator key (
secrets:write,secrets:read,actions:write) and an agent key (actions:invoke,actions:read,approvals:read)
Workspace rules setup
Create rules directory
mkdir -p .windsurfCreate rules file
Create .windsurf/rules.md in your project root.
Windsurf rules for Anlyon projects
Create .windsurf/rules.md:
# Anlyon Development Rules
## Project context
This project uses Anlyon for governed agent execution:
- Production calls are Anlyon actions the agent invokes by name. The agent never holds a provider credential.
- Sensitive actions require a human approval before Anlyon makes the call.
- Every invocation has a recorded outcome. `unknown` is reconciled, never retried blindly.
## SDK reference
### Installation
```bash
npm install @anlyonhq/sdk
```
### Two clients, two keys
```typescript
import { Client } from '@anlyonhq/sdk';
// Defines actions and vaults credentials. Not the key the agent runs with.
const ops = new Client({ apiKey: process.env.ANLYON_OPERATOR_KEY! });
// The agent's key. Can invoke; cannot read a credential or rewrite an action.
const anlyon = new Client({ apiKey: process.env.ANLYON_AGENT_KEY! });
```
## Essential patterns
### Define an action (operator, once)
```typescript
await ops.secrets.put('HELPDESK_TOKEN', {
value: process.env.HELPDESK_TOKEN!,
allowedHosts: ['api.example.com'],
});
await ops.actions.create({
name: 'update-ticket-priority',
method: 'PATCH',
urlTemplate: 'https://api.example.com/tickets/{{input.ticketId}}',
headers: { Authorization: 'Bearer {{secret:HELPDESK_TOKEN}}' },
inputSchema: {
type: 'object',
properties: { ticketId: { type: 'string' }, priority: { type: 'string' } },
required: ['ticketId', 'priority'],
},
requiresApproval: true,
});
```
### Invoke by name (agent)
```typescript
const { data } = await anlyon.actions.invoke(
'update-ticket-priority',
{ ticketId: 'T-1042', priority: 'high' },
{ idempotencyKey: 'ticket-T-1042-priority-high' },
);
if (data!.pendingApproval) {
const decided = await anlyon.approvals.waitForDecision(data!.approvalId!);
// decided.status: 'approved' | 'denied' | 'expired' | 'pending' (timed out, still decidable)
}
```
### Decide (operator key with approvals:decide)
```typescript
const pending = await ops.approvals.list({ status: 'pending' });
await ops.approvals.approve('apr_...', { note: 'Checked the ticket.' });
await ops.approvals.deny('apr_...', { note: 'Wrong customer.' });
```
### Read outcomes
```typescript
const { data: invocation } = await anlyon.actions.invocation('inv_...');
// invocation.status: 'succeeded' | 'failed' | 'unknown' | ...
// 'failed' means the call did not happen. 'unknown' means it may have: reconcile, do not retry.
```
### Webhook verification
```typescript
// Next.js App Router (recommended)
import { serve } from '@anlyonhq/sdk/nextjs';
export const { POST } = serve(async (request) => {
const data = await request.json();
return { success: true };
});
// Express/Node
import { Receiver } from '@anlyonhq/sdk';
const receiver = new Receiver({
signingSecret: process.env.ANLYON_SIGNING_SECRET!,
});
const isValid = receiver.verify({ signature, body });
```
## Error handling
```typescript
import {
AuthenticationError,
RateLimitError,
QuotaExceededError,
ValidationError,
NotFoundError,
} from '@anlyonhq/sdk';
try {
await anlyon.actions.invoke('update-ticket-priority', { ticketId: 'T-1042', priority: 'high' });
} catch (error) {
if (error instanceof RateLimitError) {
// Wait and retry the request (the SDK already retries transport errors)
} else if (error instanceof QuotaExceededError) {
// Wait for the reset or free up room
}
}
```
## Environment variables
```env
ANLYON_OPERATOR_KEY=operator_key_never_in_agent_code
ANLYON_AGENT_KEY=agent_key
ANLYON_SIGNING_SECRET=your_signing_secret
```
## Best practices
1. Production calls go through actions. Never call a provider API with a credential from agent code.
2. Reference credentials as `{{secret:NAME}}` in the action definition. Never print or log one.
3. Set an idempotency key on every invocation that must not run twice.
4. Treat `unknown` as "may have happened". Reconcile with the destination before anyone retries.
5. Use a staging key while building. It cannot reach production data.
6. Always verify webhook signatures.
## API base URL
- Production: https://api.anlyon.com
- Docs for agents: https://docs.anlyon.com/llms.txt
Using Cascade for Anlyon development
Windsurf’s Cascade understands your project context from the rules file. Here are common tasks:
Defining an action from an existing API call
Ask Cascade:
This service calls the helpdesk API directly with HELPDESK_TOKEN. Turn that call into an Anlyon action defined with the operator client, then replace the direct call with actions.invoke from the agent client.
Expected output:
// setup/actions.ts (operator, run once)
import { Client } from '@anlyonhq/sdk';
const ops = new Client({ apiKey: process.env.ANLYON_OPERATOR_KEY! });
await ops.secrets.put('HELPDESK_TOKEN', {
value: process.env.HELPDESK_TOKEN!,
allowedHosts: ['api.example.com'],
});
await ops.actions.create({
name: 'update-ticket-priority',
method: 'PATCH',
urlTemplate: 'https://api.example.com/tickets/{{input.ticketId}}',
headers: { Authorization: 'Bearer {{secret:HELPDESK_TOKEN}}' },
inputSchema: {
type: 'object',
properties: { ticketId: { type: 'string' }, priority: { type: 'string' } },
required: ['ticketId', 'priority'],
},
requiresApproval: true,
});
// agent/tickets.ts
const anlyon = new Client({ apiKey: process.env.ANLYON_AGENT_KEY! });
export async function raisePriority(ticketId: string) {
const { data } = await anlyon.actions.invoke(
'update-ticket-priority',
{ ticketId, priority: 'high' },
{ idempotencyKey: `priority-${ticketId}` },
);
return data!.pendingApproval ? { parked: data!.approvalId } : { status: data!.responseStatus };
}
Waiting for approval
Ask Cascade:
After invoking a gated action, wait up to ten minutes for the decision with approvals.waitForDecision and handle approved, denied, expired and still-pending separately
Building a reviewer tool
Ask Cascade:
Write a small CLI that lists pending Anlyon approvals with the operator key and approves or denies one by id with a note
Reconciling unknown outcomes
Ask Cascade:
Write a job that lists Anlyon invocations with status unknown, checks each one against the destination, and records the outcome with actions.resolveInvocation
Best practices with Cascade
Provide context
Mention “using Anlyon actions” when asking for code that calls a production API
Be specific
Say which client (operator or agent) the code runs with, so the right key is used
Request verification
Always ask for webhook signature verification in handler code
Include error handling
Request proper error handling with SDK typed errors, and separate failed from unknown
Resources
- Anlyon SDK: the official TypeScript SDK on npm
- TypeScript SDK Guide: Complete SDK documentation
- Actions: Templates, input schemas, versions and gates

