
# Actions Developer Guide

This guide provides step-by-step instructions for developers to extend the Actions system by creating custom blocks, triggers, and implementing new functionality.

> **User Documentation**: See [Actions User Guide](/actions/user-guide) for non-technical workflow creation instructions.

## Creating Custom Action Blocks

Action blocks are the core processing units in workflows. Here's how to create new ones:

### 1. Define Block Constants

First, define your block name constant in the shared package:

```typescript
// packages/shared/src/constants.ts (or appropriate constants file)
export const BLOCK_TRANSFORM_DATA = "transform-data";
export const BLOCK_HTTP_REQUEST = "http-request"; 
export const BLOCK_DATABASE_QUERY = "database-query";
```

### 2. Implement Block Function

Create your block implementation following the `ActionBlockImpl` interface:

```typescript
// packages/server/src/actions/blocks/transform-data.block.ts
import type { ActionBlockResult } from "@smallstack/shared";

export const BLOCK_TRANSFORM_DATA = "transform-data";

export async function transformDataBlockFn(
  socketName: string,
  data: any
): Promise<ActionBlockResult> {
  try {
    // Validate input socket
    if (socketName !== "input") {
      throw new Error(`Unexpected socket name: ${socketName}`);
    }

    // Perform transformation logic
    const transformedData = {
      ...data,
      transformedAt: new Date().toISOString(),
      processed: true
    };

    return {
      socketName: "output",  // Define output socket
      data: transformedData,
      logs: [{
        timestamp: Date.now(),
        message: `Data transformed successfully for ${Object.keys(data).length} fields`,
        level: "info"
      }]
    };
  } catch (error) {
    return {
      socketName: "error",
      logs: [{
        timestamp: Date.now(),
        message: `Transform failed: ${error.message}`,
        level: "error"
      }]
    };
  }
}
```

### 3. Register Block in Registry

Add your block to the server registry:

```typescript
// packages/server/src/actions/create-server-block-registry.ts
import { ActionBlockRegistry } from "@smallstack/shared";
import { BLOCK_SEND_MAIL, sendMailBlockFn } from "./blocks/send-mail.block";
import { BLOCK_TRANSFORM_DATA, transformDataBlockFn } from "./blocks/transform-data.block";

export function createServerBlockRegistry(): ActionBlockRegistry {
  const serverActionBlocksRegistry = new ActionBlockRegistry();
  
  // Existing blocks
  serverActionBlocksRegistry.registerBlock(BLOCK_SEND_MAIL, {
    execute: sendMailBlockFn
  });
  
  // Your new block
  serverActionBlocksRegistry.registerBlock(BLOCK_TRANSFORM_DATA, {
    execute: transformDataBlockFn
  });
  
  return serverActionBlocksRegistry;
}
```

## Block Implementation Patterns

### Configuration-Based Blocks

Many blocks need configuration parameters:

```typescript
interface HttpRequestConfig {
  url: string;
  method: "GET" | "POST" | "PUT" | "DELETE";
  headers?: Record<string, string>;
  timeout?: number;
}

export async function httpRequestBlockFn(
  socketName: string,
  data: any,
  configuration?: HttpRequestConfig
): Promise<ActionBlockResult> {
  const config = configuration || {};
  
  if (!config.url) {
    throw new Error("URL is required in block configuration");
  }

  try {
    const response = await fetch(config.url, {
      method: config.method || "GET",
      headers: config.headers,
      body: config.method !== "GET" ? JSON.stringify(data) : undefined,
      signal: AbortSignal.timeout(config.timeout || 5000)
    });

    const responseData = await response.json();

    return {
      socketName: response.ok ? "success" : "error",
      data: responseData,
      logs: [{
        timestamp: Date.now(),
        message: `HTTP ${config.method} to ${config.url}: ${response.status}`,
        level: response.ok ? "info" : "warn"
      }]
    };
  } catch (error) {
    return {
      socketName: "error",
      logs: [{
        timestamp: Date.now(),
        message: `HTTP request failed: ${error.message}`,
        level: "error"
      }]
    };
  }
}
```

### Multi-Output Blocks

Blocks can have multiple output sockets for different scenarios:

```typescript
export async function conditionalBlockFn(
  socketName: string,
  data: any
): Promise<ActionBlockResult> {
  const value = data.value;
  
  if (typeof value === "number") {
    return {
      socketName: value > 0 ? "positive" : "negative",
      data: { result: value, type: "number" },
      logs: [{
        timestamp: Date.now(),
        message: `Number ${value} routed to ${value > 0 ? "positive" : "negative"} path`,
        level: "info"
      }]
    };
  }
  
  return {
    socketName: "invalid",
    data: { error: "Expected number input" },
    logs: [{
      timestamp: Date.now(),
      message: `Invalid input type: ${typeof value}`,
      level: "warn"
    }]
  };
}
```

### Async Operations

Handle long-running operations properly:

```typescript
export async function databaseQueryBlockFn(
  socketName: string,
  data: any
): Promise<ActionBlockResult> {
  const startTime = Date.now();
  
  try {
    // Simulate database operation
    const results = await performDatabaseQuery(data.query, data.params);
    
    const duration = Date.now() - startTime;
    
    return {
      socketName: "results",
      data: { results, duration },
      logs: [{
        timestamp: Date.now(),
        message: `Query completed in ${duration}ms, returned ${results.length} rows`,
        level: "info"
      }]
    };
  } catch (error) {
    return {
      socketName: "error", 
      logs: [{
        timestamp: Date.now(),
        message: `Database query failed after ${Date.now() - startTime}ms: ${error.message}`,
        level: "error"
      }]
    };
  }
}
```

## Testing Action Blocks

### Unit Testing

Create focused tests for individual blocks:

```typescript
// packages/server/src/actions/blocks/transform-data.block.test.ts
import { describe, expect, test } from "vitest";
import { transformDataBlockFn } from "./transform-data.block";

describe("Transform Data Block", () => {
  test("transforms data successfully", async () => {
    const inputData = { name: "John", age: 30 };
    
    const result = await transformDataBlockFn("input", inputData);
    
    expect(result.socketName).toBe("output");
    expect(result.data).toMatchObject({
      name: "John",
      age: 30,
      processed: true
    });
    expect(result.data.transformedAt).toBeDefined();
    expect(result.logs).toHaveLength(1);
    expect(result.logs[0].level).toBe("info");
  });

  test("handles invalid socket name", async () => {
    const result = await transformDataBlockFn("wrongSocket", {});
    
    expect(result.socketName).toBe("error");
    expect(result.logs[0].level).toBe("error");
    expect(result.logs[0].message).toContain("Unexpected socket name");
  });
});
```

### Integration Testing

Test blocks within the execution context:

```typescript
// packages/shared/src/actions/action-executor.test.ts
test("can execute transform data action", async () => {
  const blockRegistry = new ActionBlockRegistry();
  blockRegistry.registerBlock(BLOCK_TRANSFORM_DATA, {
    execute: transformDataBlockFn
  });

  const actions: Action[] = [{
    triggers: [{ triggerName: "testTrigger", id: "trigger1" }],
    blocks: [{
      id: "block1",
      blockName: BLOCK_TRANSFORM_DATA,
      configuration: {}
    }],
    connections: [{
      sourceBlockId: "trigger1",
      sourceSocketName: TRIGGER_SOCKET_NAME,
      targetBlockId: "block1",
      targetSocketName: "input"
    }]
  }];

  const results = await executeActionsByTrigger({
    triggerName: "testTrigger",
    triggerData: { name: "Test", value: 42 },
    blockRegistry,
    actions
  });

  expect(results).toHaveLength(1);
  expect(results[0].success).toBe(true);
});
```

## Adding Custom Triggers

### 1. Define Trigger Type

Add new trigger types to the enum:

```typescript
// packages/shared/src/actions/action-trigger.ts
export enum ActionTriggers {
  DOCUMENT_CREATED = "documentCreated",
  DOCUMENT_UPDATED = "documentUpdated", 
  DOCUMENT_DELETED = "documentDeleted",
  
  // New triggers
  USER_LOGIN = "userLogin",
  PAYMENT_PROCESSED = "paymentProcessed",
  SCHEDULED_TIME = "scheduledTime"
}
```

### 2. Implement Trigger Detection

Create server-side logic to detect and fire triggers:

```typescript
// packages/server/src/services/user-trigger.service.ts
import { executeActionsByTrigger } from "@smallstack/shared";
import { createServerBlockRegistry } from "../actions/create-server-block-registry";

export class UserTriggerService {
  private blockRegistry = createServerBlockRegistry();

  async onUserLogin(userId: string, userData: any) {
    // Get actions for this tenant that have userLogin triggers
    const actions = await this.getActionsByTrigger("userLogin");
    
    if (actions.length > 0) {
      const results = await executeActionsByTrigger({
        triggerName: "userLogin",
        triggerData: { userId, userData, timestamp: Date.now() },
        blockRegistry: this.blockRegistry,
        actions
      });
      
      // Log or handle results as needed
      console.log(`Executed ${results.length} user login actions`);
    }
  }
  
  private async getActionsByTrigger(triggerName: string): Promise<Action[]> {
    // Implementation depends on your data layer
    // Return actions that have triggers matching the triggerName
  }
}
```

### 3. Integrate with Application Events

Connect triggers to your application's event system:

```typescript
// In your authentication service
export class AuthService {
  constructor(private userTriggerService: UserTriggerService) {}
  
  async loginUser(credentials: LoginCredentials) {
    const user = await this.validateCredentials(credentials);
    
    // Perform login logic...
    
    // Trigger Actions
    await this.userTriggerService.onUserLogin(user.id, {
      email: user.email,
      loginTime: new Date(),
      ipAddress: credentials.ipAddress
    });
    
    return user;
  }
}
```

## Best Practices

### 1. Error Handling

- **Always return a result**: Never throw unhandled exceptions
- **Provide meaningful logs**: Include context and error details
- **Use appropriate log levels**: `info`, `warn`, `error`
- **Handle socket validation**: Check input socket names

### 2. Performance

- **Keep blocks lightweight**: Avoid heavy computations
- **Use async/await properly**: Don't block unnecessarily
- **Implement timeouts**: For external API calls
- **Consider memory usage**: Don't store large objects

### 3. Configuration

- **Validate configuration**: Check required parameters early
- **Provide defaults**: For optional configuration values
- **Document configuration**: Clear parameter descriptions
- **Type configuration**: Use TypeScript interfaces

### 4. Multi-Tenancy

- **Respect tenant boundaries**: Never access other tenant's data
- **Include tenant context**: Pass tenant information to external services
- **Validate access**: Ensure user has permission for actions
- **Isolate data**: Keep tenant data completely separate

### 5. Testing

- **Test happy path**: Normal execution scenarios
- **Test error cases**: Invalid inputs, network failures
- **Test configuration**: Various configuration combinations
- **Test multi-tenancy**: Data isolation and access control

## Common Patterns

### 1. Data Transformation Pipeline

```typescript
Action with sequence:
Trigger → Transform Block → Validate Block → Save Block
```

### 2. Conditional Workflows

```typescript
Action with branching:
Trigger → Condition Block → { Success Block | Error Block }
```

### 3. External Integration

```typescript
Action with external calls:
Trigger → HTTP Block → Parse Block → Store Block
```

### 4. Notification Workflows

```typescript
Action with notifications:
Trigger → Format Block → { Email Block | SMS Block | Slack Block }
```

## Debugging Actions

### 1. Enable Detailed Logging

```typescript
export async function debuggableBlockFn(
  socketName: string,
  data: any
): Promise<ActionBlockResult> {
  const logs: ActionExecutionResultLog[] = [];
  
  logs.push({
    timestamp: Date.now(),
    message: `Starting execution with socket: ${socketName}`,
    level: "info"
  });
  
  logs.push({
    timestamp: Date.now(),
    message: `Input data: ${JSON.stringify(data)}`,
    level: "info"
  });
  
  // ... block logic ...
  
  return {
    socketName: "output",
    data: result,
    logs
  };
}
```

### 2. Use Development Tools

- **Unit tests**: Test blocks in isolation
- **Integration tests**: Test full workflows
- **Console logging**: During development
- **Execution results**: Review logs from actual runs

### 3. Common Issues

- **Socket name mismatches**: Ensure connections use correct socket names
- **Missing block registration**: Verify blocks are registered in registry
- **Configuration errors**: Validate block configuration parameters
- **Async handling**: Properly await asynchronous operations