Membrane Framework

Conceptual model of Membrane

Membrane Framework is used to define how your app or agent interacts with external apps.

Membrane Elements

Your app, external apps, and everything in between is represented in Membrane as Elements.

  • External Apps for apps your product interacts with.
    • Connectors are use to authenticate in and interact with external apps.
    • Integrations represent relationship between your product and a given external app: connector used, your OAuth parameters, application-level (global) webhooks, etc.
    • Connections are containers for credentials, configuration, and logic for accessing external apps on behalf of a specific customer.
  • Packages contain integration logic organized in specialized functional elements:
    • Actions are used to make synchronous requests between apps.
    • External Events and Internal Events represent things that happen in external apps and your app respectively.
    • Flows handle multi-step asynchronous logic triggered by events, via API, or on schedule.
    • Data Collections let you work with data in external apps as if it was a table in a database: read and write data, explore metadata, and subscribe to data change events.
    • Data Sources act as pointers to Data Collections, letting you build data-related logic not attached to a specific collection.
    • Field Mappings transform data when reading from or writing to Data Sources.
    • Data Links let you track relationships between data objects in your product and corresponding objects in external apps.
    • Internal Data Schemas represent data structures in your app.
  • Customers represent users, organizations, and other similar entities in your product.

Membrane Functions

Membrane Functions implement business logic inside Membrane Elements. For example, things like "run an action", "subscribe to event", or "search records in a data collections" are functions.

You can think of functions as pieces of code that run inside Membrane and have access to the relevant execution context: API clients, current customer and connection, current Membrane Element and its dependencies, etc.

In many cases, functions are Javascript / Typescript code that looks like this:

module.exports = async function({externalApiClient, input}) {
  // Do something with external app
  externalApiClient.post('/some-endpoint', input)
  // Log things for visibility
  console.log('I am a log')
  // Return the result
  return 'I am function output'
}

Additionally to Javascript / Typescript code, there are other types of functions that are simpler and offer more guardrails at the expense of flexibility.

Read more about functions and available function types here: Functions.

Membrane Layers

Membrane Elements can exist on one of three layers:

  • Universal Layer: elements that work across multiple external apps in a consistent way. For example, if you need a consistent way to work with users in external apps or a standard way to download files, you would implement this with universal elements. These elements have neither integrationId nor connectionId property set.
  • Integration Layer: elements related to a specific integrations. Elements in this layer define how you interact with a specific External App in a context of a specific Integration. These elements always have integrationId property.
  • Connection Layer: elements related to a specific customer and connection. They can be defined either by you or by your customers directly. These elements always have connectionId property.

Implementations

Most universal elements need to be implemented for each individual integration. For example, universal Action "Create Task" can define standard inputs and outputs for creating tasks in external apps, but each integration requires its own API request and response mapping. To achieve this, you:

  • Create a universal Action that defines only input and output schemas, and possibly an implementation that uses other universal elements like Data Sources and Field Mappings.
  • Create any number of integration-level Actions that have parentId property pointing to the universal action and integrationId property pointing to each integration you want to implement the action for. These actions contain application-specific implementations that fit the universal definition.

Customizations

Integration-layer elements may benefit from customization for each individual connection. For example, if you implemented an integration-layer Create Task action, each of your customers may want to configure different project id and tags to create tasks with. To achieve this, you can create a connection-layer element that points to the integration-layer element with its parentId property and have a corresponding connectionId property. This element will inherit the implementation from the integration-level element by default and can be further customized by you or your customers.

Connection-layer elements will be automatically updated when their parent integration-layer element changes, unless they were customized. If they were customized, you can reset them to the default value.

You can have multiple customizations of a given integration-layer element and differentiate them using the instanceKey property. For example, if customers can have multiple projects in your app, you can create an instance of create-task action for each project, let your customers configure them differently, and identify the right one using instanceKey that matches the project id on your side.

Addressing Elements

Every element has the following identifying properties:

  • id that is unique to a Membrane cluster.
  • uuid that is unique to the workspace and can be used to identify the same element when migrating it between workspaces.

Additionally, many elements have key property that is human-readable and should be used to address elements in code (your product code on Membrane functions). When addressing elements in Membrane, you provide a selector that can be either id, uuid, or key of a given element.

Keys

Addressing elements by key is convenient, but requires a few rules to avoid naming conflicts:

  • Universal elements must have unique key among their element type (i.e. you can't have two universal Actions with key create-task).
  • Integration-layer elements must have unique key among their type and integration (i.e. you can have one Action with key create-task for each integration).
  • Connection-level elements don't have keys (except ones inherited from their parent elements - see below).

When addressing integration-layer elements, you should always prefix the element key with integration key, i.e. if Action key is create-task and it belongs to integration with key github, you should use selector github.create-task when addressing it to avoid accidental naming conflicts with other integrations.

Addressing implementations and customizations

When you need to address a specific implementation of universal element or customization of integration-level element, you can use the following request parameters:

  • integrationKey or integrationId for getting a child element belonging to a given integration.
  • connectionId for getting a child element belonging to a given connection.
  • layer that can have value integration or connection to get element for a specified layer.
  • instanceKey to address a specific instance of a connection-layer customization.

Whenever possible, Membrane will try to guess the element you need. For example, if you request a connection-layer element and provide integrationKey, Membrane will find a default connection for the specified integration for the current customer and return it. If the current customer has no connections or multiple connections for the specified integration, an error will be returned. This lets you simplify requests based on your expected Membrane configuration (i.e. whether you allow multiple connections to the same integration per customer or if you have multiple instances of connection-layer elements per connection).