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.

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»

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»

Apps are defined using the defineApp function and follow the AppSchema interface:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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»

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»

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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»

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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»

Configuration values can be accessed in app callbacks through the input.app.config object:

1
2
3
4
5
6
7
8
9
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»

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

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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»

Called when an app is being removed or needs to clean up resources:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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»

Called when a block within the app sends a message to the app:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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»

Apps can handle incoming HTTP requests through the http component:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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»

Apps can define schedules to perform regular operations:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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»

Apps can handle timer events through the onTimer method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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»

Apps can store state using the key-value (KV) storage service. This provides persistent storage across executions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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)
});

Best practices for app development»

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»

  • 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»

  • 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»

  • 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»

  • 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»

  • 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»

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