Skip to content
Anlyon
Esc
↑↓navigate↵open⌘Jpreview
On this page

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 .windsurf

Create 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

Was this page helpful?