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”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”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”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”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”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”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”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”Apps have lifecycle methods that are called at different points during an app’s existence:
onSync
Section titled “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”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”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 ${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”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”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”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”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”Configuration
Section titled “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”- 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”- 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”- 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”- 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”- 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”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