# Apps in Flows

Apps in Flows are the fundamental building blocks for creating integrations and automations. They’re collections of reusable components (entities) that encapsulate functionality for interacting with external services, processing data, and implementing business logic.

## Introduction to apps
[Section titled “Introduction to apps”](#introduction-to-apps)
Apps in Flows are written in TypeScript and executed in a Node.js runtime environment. They provide a structured way to build, configure, and deploy integrations with various services and systems. Each app can have multiple instances (installations), allowing you to connect to different accounts or environments of the same service.

Key characteristics of Flows apps:

 - Written in TypeScript and executed in a Node.js runtime environment
 - Contain one or more entities (reusable components)
 - Can be configured with user-provided settings
 - Can maintain state across executions
 - Can be installed multiple times with different configurations

## App structure
[Section titled “App structure”](#app-structure)
An app defines the overall integration and serves as a container for entities. The app definition specifies app-level behaviors and configuration, while referencing the entities that implement specific functionalities.

### Defining an app
[Section titled “Defining an app”](#defining-an-app)
Apps are defined using the `defineApp` function and follow the `AppSchema` interface:


**

```
import { defineApp } from "@slflows/sdk/v1";
export default defineApp({  name: "My Integration App",  installationInstructions: "Configure with your API credentials.",
  // App configuration options  config: {    apiKey: {      name: "API Key",      description: "Your service API key",      type: { type: "string" },      required: true,    },  },
  // Lifecycle methods  onSync: async (input) => {    // Setup code when app is synced    return { newStatus: "ready" };  },
  onDrain: async (input) => {    // Cleanup code when app is drained    return { newStatus: "drained" };  },
  // Block definitions  blocks: {    // Block definitions go here  },
  // Optional components  http: {    onRequest: async (input) => {      // Handle HTTP requests to the app    },  },
  // Additional capabilities  schedules: {    // App-level scheduled tasks  },
  // Handle internal messages from blocks  onInternalMessage: async (input) => {    // Process messages from blocks  },
  // Handle timer events  onTimer: async (input) => {    // Handle timer triggers  },});
```

## App configuration
[Section titled “App configuration”](#app-configuration)
Apps can be configured with user-provided settings, which are defined in the `config` property of the app schema. These configuration values are provided by users when installing an app.

### Defining configuration options
[Section titled “Defining configuration options”](#defining-configuration-options)

**

```
config: {  apiKey: {    name: "API Key", // Human-readable name    description: "Your service API key", // Description (shown in tooltip)    type: { type: "string" }, // Data type (string, number, boolean, object, etc.)    required: true, // Whether this option is required    default: "demo-key", // Optional default value  },  region: {    name: "Service Region",    description: "The region for your service account",    type: {      type: "string",      enum: ["us-east-1", "us-west-2", "eu-west-1"],    },    required: true,  },},
```

### Common configuration types
[Section titled “Common configuration types”](#common-configuration-types)

**

```
config: {  // String input  username: {    name: "Username",    type: "string",    required: true  },
  // Password (secret) input  password: {    name: "Password",    type: "string",    required: true  },
  // Number input  timeout: {    name: "Timeout (seconds)",    type: "number",    required: true,    default: 30  },
  // Boolean input  enableLogging: {    name: "Enable Logging",    type: "boolean",    required: false,    default: true  },
  // Dropdown select  logLevel: {    name: "Log Level",    type: {      type: "string",      enum: ["debug", "info", "warn", "error"]    },    required: true,    default: "info"  },
  // Array of strings  allowedDomains: {    name: "Allowed Domains",    type: {      type: "array",      items: { type: "string" }    },    required: false  },
  // Object with properties  advancedSettings: {    name: "Advanced Settings",    type: {      type: "object",      properties: {        retryCount: { type: "number" },        cacheEnabled: { type: "boolean" }      }    },    required: false  }}
```

### Accessing configuration in code
[Section titled “Accessing configuration in code”](#accessing-configuration-in-code)
Configuration values can be accessed in app callbacks through the `input.app.config` object:


**

```
onSync: async (input) => {  const apiKey = input.app.config.apiKey;  const region = input.app.config.region;
  console.log(`App synced with API key: ${apiKey} for region ${region}`);
  // Use the configuration to set up the app  return { newStatus: "ready" };};
```

## App lifecycle
[Section titled “App lifecycle”](#app-lifecycle)
Apps have lifecycle methods that are called at different points during an app’s existence:

### onSync
[Section titled “onSync”](#onsync)
Called when an app needs to reconcile its desired state. This is where you can perform setup, validate configuration, and manage the app’s status:


**

```
onSync: async (input) => {  try {    // Validate configuration    const apiKey = input.app.config.apiKey;
    // Test API connection    const testResult = await testApiConnection(apiKey);
    // Set initial app state    await kv.app.set({      key: "installation",      value: {        syncedAt: new Date().toISOString(),        status: "active",      },    });
    // Update internal state if needed    await kv.app.set({      key: "connectionStatus",      value: "connected",    });
    return { newStatus: "ready" };  } catch (error) {    console.error("App sync failed:", error);    return {      newStatus: "failed",      customStatusDescription: `Sync failed: ${error.message}`,    };  }};
```

### onDrain
[Section titled “onDrain”](#ondrain)
Called when an app is being removed or needs to clean up resources:


**

```
onDrain: async (input) => {  try {    // Perform cleanup    const apiKey = input.app.config.apiKey;
    // Unregister webhooks, revoke tokens, etc.    await unregisterWebhooks(apiKey);
    // Revoke any access tokens    const tokens = await kv.app.get("tokens");    if (tokens?.value) {      await revokeTokens(tokens.value);    }
    // Clean up app state    await kv.app.delete(["installation", "connectionStatus", "tokens"]);
    return { newStatus: "drained" };  } catch (error) {    console.error("Cleanup failed:", error);    return {      newStatus: "draining_failed",      customStatusDescription: `Cleanup failed: ${error.message}`,    };  }};
```

### onInternalMessage
[Section titled “onInternalMessage”](#oninternalmessage)
Called when a block within the app sends a message to the app:


**

```
onInternalMessage: async (input) => {  const message = input.message.body;
  console.log(`Received message from block ${message.senderId}:`, message);
  // Process the message based on type  if (message.type === "register_webhook") {    // Store block's webhook preferences    await kv.app.set({      key: `webhooks:${message.senderId}`,      value: {        eventTypes: message.eventTypes,        registeredAt: new Date().toISOString(),      },    });
    // Confirm registration back to the block    await messaging.sendToBlocks({      blockIds: [message.senderId],      body: {        type: "webhook_registration_confirm",        eventTypes: message.eventTypes,        status: "registered",      },    });  }};
```

## HTTP handling
[Section titled “HTTP handling”](#http-handling)
Apps can handle incoming HTTP requests through the `http` component:


**

```
http: {  onRequest: async (input) => {    const { request } = input;
    // Access request details    console.log(`Received ${request.method} request to ${request.path}`);    console.log("Headers:", request.headers);
    // Route based on path    if (request.path === "/webhook" && request.method === "POST") {      // Process a webhook      await processWebhook(request);
      // Respond to the sender      await http.respond(request.requestId, {        statusCode: 200,        headers: {          "Content-Type": "application/json",        },        body: {          status: "received",          timestamp: new Date().toISOString(),        },      });    } else if (request.path === "/oauth/callback" && request.method === "GET") {      // Handle OAuth callback      const code = request.query.code;      const state = request.query.state;
      // Exchange code for token      try {        const tokens = await exchangeCodeForTokens(code);
        // Store tokens        await kv.app.set({          key: "tokens",          value: tokens,        });
        // Respond with success        await http.respond(request.requestId, {          statusCode: 200,          body: "Authentication successful! You can close this window.",        });      } catch (error) {        // Handle error        await http.respond(request.requestId, {          statusCode: 400,          body: `Authentication failed: ${error.message}`,        });      }    } else {      // Handle unknown paths      await http.respond(request.requestId, {        statusCode: 404,        body: { error: "Not found" },      });    }  }}
```

## Scheduling
[Section titled “Scheduling”](#scheduling)
Apps can define schedules to perform regular operations:


**

```
schedules: {  refreshTokens: {    description: "Refresh OAuth tokens before they expire",    definition: {      type: "cron",      cron: {        expression: "0 3 * * *", // Run daily at 3 AM        location: "UTC",      },    },    onTrigger: async (input) => {      console.log("Running token refresh at:", input.schedule.time);
      // Get current tokens      const tokens = await kv.app.get("tokens");
      if (tokens?.value && tokens.value.refreshToken) {        try {          // Refresh the token          const newTokens = await refreshOAuthTokens(tokens.value.refreshToken);
          // Store new tokens          await kv.app.set({            key: "tokens",            value: newTokens,          });
          console.log("OAuth tokens refreshed successfully");        } catch (error) {          console.error("Failed to refresh tokens:", error);        }      }    },  },  dailyMetrics: {    description: "Collect daily usage metrics",    customizable: true, // Allow users to customize this schedule    definition: {      type: "frequency",      frequency: {        interval: 6,        unit: "hours", // Every 6 hours by default      },    },    onTrigger: async (input) => {      console.log("Collecting metrics at:", input.schedule.time);
      try {        // Collect metrics from service        const metrics = await collectServiceMetrics(input.app.config);
        // Store metrics        await kv.app.set({          key: `metrics:${new Date().toISOString().split("T")[0]}`,          value: metrics,        });      } catch (error) {        console.error("Failed to collect metrics:", error);      }    },  },},
```

## Timer handling
[Section titled “Timer handling”](#timer-handling)
Apps can handle timer events through the `onTimer` method:


**

```
onTimer: async (input) => {  const { timer } = input;
  if (timer.payload.type === "retryOperation") {    const operationId = timer.payload.operationId;    const attempt = timer.payload.attempt || 1;
    console.log(`Retrying operation ${operationId}, attempt ${attempt}`);
    try {      // Try the operation again      const result = await performOperation(operationId);
      // Operation succeeded      await kv.app.set({        key: `operation:${operationId}:result`,        value: {          status: "success",          result,          completedAt: new Date().toISOString(),        },      });    } catch (error) {      console.error(`Retry attempt ${attempt} failed:`, error);
      if (attempt < 5) {        // Schedule another retry with exponential backoff        const backoffSeconds = Math.pow(2, attempt) * 30;
        await timers.app.set(backoffSeconds, {          inputPayload: {            type: "retryOperation",            operationId,            attempt: attempt + 1,          },          description: `Retry operation ${operationId}, attempt ${attempt + 1}`,        });      } else {        // Max retries reached        await kv.app.set({          key: `operation:${operationId}:result`,          value: {            status: "failed",            error: error.message,            attempts: attempt,            failedAt: new Date().toISOString(),          },        });      }    }  }};
```

## State management with app-level KV storage
[Section titled “State management with app-level KV storage”](#state-management-with-app-level-kv-storage)
Apps can store state using the key-value (KV) storage service. This provides persistent storage across executions:


**

```
import { kv } from "@slflows/sdk/v1";
// Store a valueawait kv.app.set({  key: "lastSyncTime",  value: Date.now(),});
// Retrieve a valueconst lastSync = await kv.app.get("lastSyncTime");if (lastSync?.value) {  console.log(`Last sync was at: ${new Date(lastSync.value).toISOString()}`);}
// Store multiple valuesawait kv.app.setMany([  { key: "counter", value: 42 },  { key: "status", value: "active" },]);
// List values with a prefixconst configs = await kv.app.list({ keyPrefix: "config:" });for (const pair of configs.pairs) {  console.log(`${pair.key}: ${pair.value}`);}
// Store value with expiration (TTL)await kv.app.set({  key: "temporaryToken",  value: "eyJhbGciOiJ...",  ttl: 3600, // Expires after 1 hour (in seconds)});
```

## Best practices for app development
[Section titled “Best practices for app development”](#best-practices-for-app-development)
### Configuration
[Section titled “Configuration”](#configuration)
 - Use descriptive names and helpful descriptions for configuration fields
 - Provide sensible defaults where appropriate
 - Use the correct types for configuration options (string, number, boolean, etc.)
 - Validate configuration in `onSync` and `onDrain`

### State management
[Section titled “State management”](#state-management)
 - Use app-level KV storage for data shared across blocks
 - Set TTLs for temporary data to avoid storage buildup
 - Use structured key prefixes (e.g., `oauth:tokens`, `user:123:settings`) to organize data
 - Implement proper error handling for KV operations

### Authentication
[Section titled “Authentication”](#authentication)
 - Store tokens securely in KV storage
 - Implement token refresh mechanisms before tokens expire
 - Use state parameters in OAuth flows to prevent CSRF attacks
 - Revoke tokens during app uninstallation

### Error handling
[Section titled “Error handling”](#error-handling)
 - Catch and handle errors in all callbacks
 - Provide meaningful error messages that help users understand issues
 - Implement retry mechanisms with exponential backoff for transient failures
 - Log errors for debugging but avoid exposing sensitive information

### HTTP handling
[Section titled “HTTP handling”](#http-handling-1)
 - Respond to HTTP requests quickly, especially webhooks
 - Implement verification for incoming webhooks (signatures, tokens, etc.)
 - Use proper HTTP status codes in responses
 - Structure response payloads consistently

### Scheduling and timers
[Section titled “Scheduling and timers”](#scheduling-and-timers)
 - Use cron expressions for regular, predictable schedules
 - Make scheduled tasks idempotent (safe to run multiple times)
 - Implement proper error handling in scheduled tasks
 - Use timers for delayed operations and retries

## Summary
[Section titled “Summary”](#summary)
Apps in Flows provide a powerful way to build integrations and automations with external services. By leveraging the app capabilities described in this documentation, you can create robust, maintainable, and scalable solutions.

Key takeaways:

 - Apps define the overall integration structure and configuration
 - App lifecycle methods handle creation, updates, and deletion
 - HTTP handlers allow integration with external systems
 - Schedules enable regular operations
 - Timers support delayed and recurring tasks
 - KV storage provides persistent state management
 - Internal messaging enables coordination between app and blocks