# Patterns & Common Mistakes

Common patterns to follow and mistakes to avoid when building with the ADK.

## Common Mistakes

### Import Errors

#### Wrong Import Sources

```typescript
// ❌ WRONG - Never import from these
import { z } from "zod";
import { Action } from "@botpress/sdk";
import { Workflow } from "@botpress/cli";

// ✅ CORRECT - Always import from @botpress/runtime
import { z, Action, Workflow, Conversation } from "@botpress/runtime";
```

**Why this matters:**
- `z` from `@botpress/runtime` is Botpress's internal fork of Zod (based on Zod 3.x)
- Importing from `zod` directly can cause compatibility issues
- ADK primitives (Action, Tool, Workflow, etc.) must come from `@botpress/runtime`

**Zod Version Compatibility Note:**
Botpress uses Zod 3.x internally. If adding `zod` to your project dependencies (e.g., for frontend code), use version `3.23.8` or compatible 3.x. Zod 4.x has breaking changes and is NOT compatible with the ADK.

#### Dynamic Imports

```typescript
// ❌ AVOID - Dynamic imports can cause issues
const { myAction } = await import("./actions/myAction");

// ✅ PREFER - Static imports
import { myAction } from "./actions/myAction";
```

### Handler Syntax

#### Action Handlers

Action handlers receive a props object with `input` and `client`. Both arrow functions and method syntax work fine — the handler type is `(props: ActionHandlerProps) => Promise<...>`.

```typescript
// ✅ CORRECT - Method syntax
export const myAction = new Action({
  async handler({ input }) {
    return { data: input.userId };
  },
});

// ✅ ALSO CORRECT - Arrow function syntax
export const myAction = new Action({
  handler: async ({ input }) => {
    return { data: input.userId };
  },
});
```

#### Tool Handlers

```typescript
// ✅ Tools use arrow functions
export const myTool = new Autonomous.Tool({
  handler: async ({ query, limit }) => {
    return searchResults;
  },
});
```

### State Management Mistakes

#### Conversation State Access

Conversations receive `state` as a destructured handler parameter. Do **not** use `this.state`.

```typescript
// ❌ WRONG - this.state does not exist
export const Chat = new Conversation({
  state: z.object({ count: z.number().default(0) }),
  async handler({ message }) {
    this.state.count += 1; // ❌ Wrong!
  },
});

// ✅ CORRECT - Use the state parameter
export const Chat = new Conversation({
  state: z.object({ count: z.number().default(0) }),
  handler: async ({ message, state }) => {
    state.count += 1; // ✅ Destructured from handler props
  },
});
```

#### State Reference vs Value

```typescript
// ❌ WRONG - Passing state reference to async function
handler: async ({ step, state }) => {
  const results = await processItems(state.items);
  // state.items may have changed while processItems was running
  state.items.push("new-item"); // processItems might see this!
}

// ✅ CORRECT - Pass a copy of the value
handler: async ({ step, state }) => {
  const itemsCopy = [...state.items];
  const results = await processItems(itemsCopy);
  state.items.push("new-item"); // safe, doesn't affect processItems
}

// ✅ CORRECT - Extract values before async operation
handler: async ({ step, state }) => {
  const currentCount = state.count;            // Primitive - safe
  const currentItems = [...state.items];       // Array - needs spread
  const currentConfig = { ...state.config };   // Object - needs spread

  await step("process", async () => {
    return await processData(currentCount, currentItems, currentConfig);
  });
}
```

**Rules of thumb:**
- **Primitives** (string, number, boolean): Can pass directly
- **Arrays**: Use spread `[...state.items]` or `Array.from()`
- **Objects**: Use spread `{ ...state.config }` or `structuredClone()`
- **Deep objects**: Use `JSON.parse(JSON.stringify())` or `structuredClone()`

### Workflow Step Mistakes

#### Dynamic Step Names

```typescript
// ❌ WRONG - Dynamic step names break resume
for (let i = 0; i < items.length; i++) {
  await step(`process-${i}`, async () => {
    await processItem(items[i]);
  });
}

// ✅ CORRECT - Single step with loop
await step("process-all", async () => {
  for (const item of items) {
    await processItem(item);
  }
});

// ✅ CORRECT - Or use step.map
await step.map("process-items", items, async (item) => {
  return processItem(item);
});
```

#### Missing Await on Steps

```typescript
// ❌ WRONG - Missing await
const result = step("fetch", async () => {
  return fetchData();
});

// ✅ CORRECT - Always await steps
const result = await step("fetch", async () => {
  return fetchData();
});
```

### Table Column Mistakes

#### Searchable Column Syntax

```typescript
// ❌ WRONG - .searchable() is not a Zod method
columns: {
  content: z.string().searchable()
}

// ✅ CORRECT - Object notation for searchable
columns: {
  content: {
    schema: z.string(),
    searchable: true,
  },
}
```

### Conversation ID Mistakes

```typescript
// ❌ WRONG - Missing conversationId prevents communication
await MyWorkflow.start({
  userId: user.id,
  data: "some data",
});

// ✅ CORRECT - Include conversationId for messaging
await MyWorkflow.start({
  conversationId: conversation.id,
  userId: user.id,
  data: "some data",
});
```

---

## Common Patterns

### Workflow Request Pattern

Request data from user during workflow execution:

```typescript
export const MyWorkflow = new Workflow({
  name: "interactive",
  requests: {
    topic: z.object({ topic: z.string() }),
  },
  handler: async ({ step, input }) => {
    const { topic } = await step.request(
      "topic",
      "What is the topic of the question?"
    );
    console.log(`Topic identified: ${topic}`);
  },
});
```

### RAG with Citations

Use citations to track sources for AI-generated responses:

```typescript
import { Autonomous, context } from "@botpress/runtime";

const SearchTool = new Autonomous.Tool({
  name: "search",
  input: z.string(),
  output: z.string(),
  handler: async (query) => {
    const client = context.get("client");
    const citations = context.get("citations");

    const { passages } = await client.searchFiles({
      query,
      tags: { type: "knowledge", kb: ["myKB"] },
    });

    const message = ["Here are the search results:"];
    const { tag: example } = citations.registerSource({});

    for (const p of passages) {
      const { tag } = citations.registerSource({ file: p.file.key });
      message.push(`<${tag} file="${p.file.key}">`);
      message.push(`**${p.file.tags.title}**`);
      message.push(p.content);
      message.push(`</${tag}>`);
    }

    throw new Autonomous.ThinkSignal(
      `When answering, MUST add inline citations (eg: "The price is $10${example} ...")`,
      message.join("\n")
    );
  },
});
```

### Context Access Patterns

The context API provides access to runtime services within handlers.

#### Basic Context Usage

```typescript
import { context } from "@botpress/runtime";

export const myAction = new Action({
  handler: async ({ input }) => {
    const client = context.get("client");
    const cognitive = context.get("cognitive");
    const logger = context.get("logger");

    const { passages } = await client.searchFiles({ query: input.query });
    logger.info("Search completed", { resultCount: passages.length });
    return { results: passages };
  },
});
```

#### Optional Context Keys

```typescript
export const myAction = new Action({
  handler: async ({ input }) => {
    const user = context.get("user", { optional: true });
    const conversation = context.get("conversation", { optional: true });

    if (user && conversation) {
      console.log(`User ${user.id} in conversation ${conversation.id}`);
    } else {
      console.log("Processing without user context");
    }
  },
});
```

#### Common Context Keys

**Always Available:**
- `client` — Botpress API client
- `cognitive` — AI model client
- `logger` — Structured logger
- `botId` — Current bot ID
- `configuration` — Bot configuration
- `runtime` — Runtime environment info

**Conditionally Available:**
- `user` — Current user (in conversation handlers)
- `conversation` — Current conversation (in conversation handlers, not to be used within a conversation object: use the `conversation` parameter instead)
- `message` — Incoming message (when triggered by message)
- `event` — Incoming event (when triggered by event)
- `workflow` — Current workflow (in workflow handlers)
- `integrations` — Installed integrations

#### Context in Different Handler Types

| Handler | Key context keys |
|---------|-----------------|
| **Actions** | `client`, `cognitive`, `logger`, `botId`, `configuration` |
| **Tools** | `client`, `cognitive`, `citations`, `logger`, `user`?, `conversation`? |
| **Workflows** | `client`, `workflow`, `workflowControlContext`, `logger` |
| **Conversations** | `client`, `user`, `conversation`, `message`, `event`?, `logger` |
| **Triggers** | `client`, `event`, `logger` |

---

## File Naming Conventions

```
// Actions: camelCase
src/actions/fetchUser.ts
src/actions/validateOrder.ts

// Tools: camelCase
src/tools/searchDocs.ts
src/tools/createTicket.ts

// Workflows: camelCase
src/workflows/onboardUser.ts
src/workflows/processPayment.ts

// Tables: PascalCase
src/tables/Users.ts
src/tables/Orders.ts
src/tables/AuditLog.ts

// Conversations: camelCase (prefer lowercase)
src/conversations/chat.ts
src/conversations/slack.ts

// Triggers: camelCase (prefer lowercase)
src/triggers/userCreated.ts
src/triggers/linear.ts

// Knowledge: descriptive
src/knowledge/documentation.md
src/knowledge/policies/privacy.md
```

### Export Naming

```typescript
// Actions
export const fetchUser = new Action({ name: "fetchUser" });

// Workflows
export const OnboardingWorkflow = new Workflow({ name: "onboarding" });

// Tables
export const UsersTable = new Table({ name: "users" });

// Conversations
export const Chat = new Conversation({ channel: "chat.channel" });

// Triggers - use default export
export default new Trigger({ name: "userEvents" });
```

---

## Best Practices Summary

### DO's

1. Import from `@botpress/runtime`
2. Use `state` parameter in conversation handlers (not `this.state`)
3. Pass state values by copy, not reference
4. Use stable step names in workflows
5. Always await step functions
6. Pass `conversationId` to workflows that communicate
7. Use object notation for searchable columns
8. Provide defaults in state schemas
9. Handle errors gracefully
10. Prefer parameter destructuring in handlers when available

### DON'Ts

1. Don't import from `zod` or `@botpress/sdk` directly
2. Don't use `this.state` in conversations (use the `state` parameter)
3. Don't pass state references to async functions (copy arrays/objects)
4. Don't use dynamic step names
5. Don't forget to await steps
6. Don't start workflows without `conversationId` if they need to message
7. Don't use `.searchable()` chain notation
8. Don't hardcode secrets in configuration
9. Don't use dynamic imports unless necessary

---

## Debugging Tips

### Check Imports

```bash
# Find incorrect imports
grep -r "from \"zod\"" src/
grep -r "from \"@botpress/sdk\"" src/
```

### Debug State Access

```typescript
// In conversations - use state parameter
handler: async ({ state }) => {
  console.log("Conversation state:", JSON.stringify(state, null, 2));
}

// In workflows - use state parameter
handler: async ({ state }) => {
  console.log("Workflow state:", JSON.stringify(state, null, 2));
}
```

### Trace Workflow Steps

```typescript
handler: async ({ step }) => {
  console.log("Starting workflow");

  const result = await step("step1", async () => {
    console.log("Executing step1");
    return "data";
  });

  console.log("Step1 result:", result);
}
```

---

## See Also

- [./actions.md](./actions.md) — Action handler reference
- [./conversations.md](./conversations.md) — Conversation handlers
- [./workflows.md](./workflows.md) — Workflow patterns
- [./tables.md](./tables.md) — Table column syntax
- [./context-api.md](./context-api.md) — Context API details
- [./advanced-patterns.md](./advanced-patterns.md) — Guardrails, admin auth, observability
