Skip to content

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.

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

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.

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
},
});

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.

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,
},
},
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
}
}

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" };
};

Apps have lifecycle methods that are called at different points during an app’s existence:

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}`,
};
}
};

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}`,
};
}
};

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",
},
});
}
};

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" },
});
}
};
}

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);
}
},
},
},

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 value
await kv.app.set({
key: "lastSyncTime",
value: Date.now(),
});
// Retrieve a value
const lastSync = await kv.app.get("lastSyncTime");
if (lastSync?.value) {
console.log(`Last sync was at: ${new Date(lastSync.value).toISOString()}`);
}
// Store multiple values
await kv.app.setMany([
{ key: "counter", value: 42 },
{ key: "status", value: "active" },
]);
// List values with a prefix
const 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)
});
  • 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
  • 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
  • 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
  • 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
  • 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
  • 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

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