Skip to content

Instantly share code, notes, and snippets.

@chelseasanc
Last active August 3, 2016 23:40
Show Gist options
  • Save chelseasanc/ee22109129440cb06910 to your computer and use it in GitHub Desktop.
Save chelseasanc/ee22109129440cb06910 to your computer and use it in GitHub Desktop.

Getting Started

What is a Channel?

A Channel is an interface that communicates with external APIs. The goal of a Channel is not to be a direct reflection of an API, but instead a user-friendly abstraction on top of an API. Channel development is more focused on end-user needs than on the capabilities of the API. Each Channel is made up of methods, which appear to the user as the different Event and Action cards. Each method determines how data is fetched from the API, and transforms data to the user-friendly format accepted by the front-end. Most methods use more than one API call to do this.

To define a Channel, you must write a Channel JSON file and submit this file to Azuqua for upload into the engine. At runtime, the engine will access the instructions laid out in this file to execute the Event or Action the user has designated in their FLO. The primary function of a Channel JSON file is to lay out in a linear manner the pre-defined action steps (known internally as bricks) that will execute each user-facing event or action. Using bricks, you can define what data will be fetched from the API, select the data you want to pass on to the user, and transform this data into flat JSON that can be consumed by the Azuqua front-end.

API Requirements

To add your application as an Azuqua Channel, your API must be able to give Azuqua enough insight into your application to start FLOs whenever important changes happen. The most efficient way to do this is through webhook notifications, but Azuqua can also monitor for changes by polling your API for updates on an incremental basis determined by the user's Azuqua plan.

Additionally, Azuqua needs to be able to replicate common user Actions through your API. For example, if users can copy items or link contacts to accounts inside your app, your API should allow Azuqua to do that as well. Read on for more specific requirements.

All applications:

  1. Your API must use one of the four supported authentication schemes: basic, key, OAuth 1.0, and OAuth 2.0. Session-based authentication is not supported, since users need to be able to set up authentications and let them run instead of regularly re-authenticating.
  2. Your API must support field-specific queries of your resources. For example, if your application has a contact resource, the API should allow developers to query for a contact by name and email.
  3. It is best if your API timestamps its records in a standard format (such as Unix/Epoch or ISO8601).

If your API supports webhooks (preferred)...

  1. Developers should be able to programmatically set up and tear down webhooks through the API.
  2. All users should have permissions to create webhooks through the API, but the data that triggers the webhook should be scoped to the data that the authenticating user has access to (users should not be able to set up a webhook that monitors resources they can't access).
  3. There should be a webhook notification for each time a resource is created, modified, or deleted. These notifications should be granular enough that the developer can focus on changes that involve a specific type of resource (but not only a single resource).
  4. The webhook that notifies Azuqua when a certain resource has been modified should also have information about what exactly was changed. For example, if there is a webhook notification for whenever a resource of a certain type is modified, the webhook should include information about which attributes were modified and what the new values of those attributes are.

If your API does not support webhooks...

  1. Developers should be able to query a feed of all the updates that have happened in the authenticated instance of the application, including whenever a record is created, modified, or deleted.
  2. If possible, each update in this feed should have a unique, incrementing ID. When the developers are querying the API, they should be able to pass the ID of the last update they looked at and receive all of the updates that have happened since then. It is also possible to use a timestamp instead of an ID to query for fresh records, but timestamps are not always specific enough and may result in duplicate FLO executions for a single record.
  3. Developers should be able to filter their query by the type of resource they wish to monitor.
  4. Developers should be able to filter their query by the exact type of update that has occurred (e.g. record deleted, name updated, assigned to member, record parent changed, etc.).
  5. If the update that has occurred in the application involves the value of a field being changed, the API should indicate what field was changed and what the new value is.

If your application supports custom resource types...

  1. Developers must be able to query your API for a list of all resource types.
  2. Developers must be able to query your API for the "shape" of each resource type, and get back a list of fields that is possible for the user to edit.

If your application supports custom fields...

  1. Developers must be able to query your API for a list of the custom fields associated with the authenticated instance of the application.
  2. The custom fields should have both a unique identifier and a user-friendly name. It is possible for these two properties to be the same if custom field names are enforced as unique in your application.

Channel Structure

Each Channel has 5 sections:

  • The General section, where you define general information about the Channel, such as the display name of the Channel, and the version number.
  • The Authentication section, where you define the Authentication schema of the Channel. Azuqua supports 3 authentication schemes: basic, OAuth (both 1 and 2) and custom. Once users have set up an authentication configuration for a Channel, Azuqua will save their credentials so this configuration can be used again.
  • The Events section, where you define any Event cards you want in your Channel. Events are the methods that start FLOs. Each FLO must have a starting Event, and every time the Event detects new records the FLO will run.
  • The Actions section, where you define any Action cards you want in your Channel. Actions are the methods that make up the majority of a FLO, and execute whenever the starting Event occurs.
  • The Metadata section, where you define any helper methods you need to execute your Events and Actions. While Events and Actions are turned into cards that are visible to the user, helper methods are never seen by the user and always execute under the hood.

Designing Your Channel

Before you build your Channel, you should gather information about real users' integration scenarios. The best Channels are built around user scenarios, rather than the capabilities of APIs. However, this section will cover a few common practices in Channel design to help you get started.

Determining your core "resources"

At its core, any application deals with the creation or modification of "resources." Users typically don't think of applications this way. When they are building a new task list, or adding a contact to their address book, they don't think of tasks and contacts as different kinds of "resources" that the application is creating for them.

However, APIs are frequently organized around "resources," with methods built around each of the resource types the application handles. The first step in build a Channel is figuring out which resources you want your Channel to monitor and modify (these are not always the same). For example, you may want your users to be able to monitor for new customers in your application, but not want them to create customers themselves since it involves handling sensitive information.

Your Channel does not have to include every kind of resource your application works with. Think instead about the resources your users directly interact with the most, and start with those. It's better to start with a small Channel and develop to accommodate more use cases, then to build a large but confusing Channel off a set of assumptions about what users need.

Designing your Events and Actions.

Most Channels include at least two Events and three Actions for every resource type. These five basic cards are:

  • New Resource (Event): this card will trigger a FLO whenever a new resource of this type is created.
  • Resource Updated (Event): this card will trigger a FLO whenever any resource of this type is updated.
  • Create Resource (Action): this card will create a new resource of this type as part of a FLO.
  • Read Resource (Action): this card will take in the unique identifier of a resource (frequently an ID or URL) and and return information about it as part of a FLO.
  • Update Resource (Action): this card will take in the unique identifier of a resource (frequently an ID or URL) and update the resource's information as part of a FLO.

Additionally, many Channels also include three other cards when the API supports these Actions and the user scenarios require them:

  • Deleted Resource (Event): this card will trigger a FLO whenever any resource of this type is updated.
  • Search Resources (Action): this card will take information about a resource and search for the unique identifier of a resource with matching information. Frequently, it is used in conjunction with a "Read Resource" card so users can look up a record using information the possess and get other information about it. For example, if a user knows the email of a contact but wants to get the contact's first name and last name, they might use a "Search Contacts" card to find the ID of the contact resource that matches the email, then drag that ID to a "Read Contact" card to find the first name and the last name.
  • Delete Resource (Action): this card will take in the unique identifier of a resource (frequently an ID or URL) and delete the resource as part of a FLO. For this reason, this kind of card is more rare, since it makes it possible for users to accidentally permanently delete records.

Although these guidelines provide an outline for your Channel, the best Channels include cards that allow users to mimic common actions they take in your app. For example, if moving resources on and off lists is a common action in your app, you might want to have "Add Resource to List" and "Remove Resource from List" Actions in your Channel.

Choosing Your Fields

Often, the fields you put on your cards are determined by the limitations of your application's API. However, you should make decisions about which fields the users need and don't need. It is often helpful to have an idea of the fields you want your card to take in and return before you start building, to at least provide guidance.

As a general rule of thumb, most Events include these fields when possible:

  • Timestamps (time created, time updated, etc.)
  • Information about the user who created the resource
  • Information about the user assigned to the resource
  • Information about the user who updated the resource (in the case of an "Updated" or "Deleted" Event)
  • The unique identifier of the resource
  • The URL of the resource
  • Any basic information about the resource that the user will be able to see in the UI (such as title, parent resource title, description, etc.)

Actions are harder to generalize, since depending on what your Action does, it could have very different inputs and outputs from other Actions. However, Actions must have "symmetry," meaning that if one Action requires a unique ID to update or delete a resource, other Actions should provide that ID--when you create a record, you get back the ID you'll need to update it later.

Starting a new Channel Project

After you've laid out the general shape of the Channel, it's time to start a new Channel project. To start a new project, go to the dropdown menu in the left-hand corner of the tool bar, and select "New Channel." The Channel Builder will open a new Channel file in your workspace, and take you to the "General" section.

In this section of the Channel, you should fill out certain basic information, including:

  • name, the display name of your Channel that will appear in the Azuqua UI. This can be changed at any time.
  • description, the description of your Channel. This will not appear in the UI, but it may be used for internal purposes.
  • version, the current version number of the Channel. Read more about Version Control below.
  • type, must be set to "channel"
  • recurrence, must be set to 1

Now, save your Channel by clicking the "Save" button in the right-hand side of the tool bar. The first time you save a Channel, you will be asked to enter a unique draft name for your Channel. This is not your display name--this name will not appear to the user, but it will identify your Channel to the Azuqua engine, so it must be unique. If you have filled in a display name already, the Azuqua Channel Builder will suggest a name for you. Normally, the draft name is just the name of your application, one word, lowercase. Once you hit "Submit," your Channel will be saved and you will now see the unique draft name in your Channels dropdown.

Version Control

Azuqua uses Semantic Versioning to version Channels. You have three numbers: a major version number, a minor version number, and a patch number. Each number is separated by decimals, and they provide information about the magnitude of the change made in each version.

For example, the first time you build a Channel, you might submit version number 1.0.0. Then, you need to make a patch to fix a bug, so you submit version 1.0.1 and 1.0.2 as you debug your work. Later on, you decide to add a card. Since this is a bigger change but it's still backward-compatible, you release version 1.1.0. Then in a few months, your application gets a new version of the Channel and it needs a complete rebuild. This will be released as version 2.0.0 and so on.

How your team uses version numbers to track changes is up to you. Currently, the Azuqua Engine does not pay any attention to version number, it only runs FLOs on the most recently published version of the Channel.

While you are working on a Channel, every time you save edits they over-write the old file. To capture a version of a Channel, increment the version number and hit "Submit." This will build the current draft of the Channel and save it in a separate location, so it can't be overwritten. It will now appear in the Control Panel so an administrator can deploy it to an environment.

Authentication

Introduction

In the Azuqua designer, users can connect an account to a Channel, set up their FLO, and then let it run. Azuqua handles all the hard work of authentication, and securely stores any pertinent authentication information (like credentials, API keys, or access keys obtained from an OAuth exchange) so it can be used whenever the FLO runs.

Azuqua supports 4 kinds of authentication: Basic, Key/Secret, OAuth 1.0, and OAuth 2.0. Additionally, you may define an authentication object that takes in custom parameters that you can then send over as part of your request to the API.

Basic Authentication

Use basic authentication when all you need to grant access to the API is a username and password, sent over in the headers or body of the request.

Template:

{
	"type": "",
	"authparams": {
		"username": {
			"type": "",
			"displayname": ""
		},
		"password": {
			"type": "",
			"displayname": ""
		}
	}
}

Fields:

  • type, must be set to 'basic' for basic authentication, and 'custom' for custom
  • authparams, an object that contains the authentication parameters users will enter. For Basic auth, these are usually "username" and "password"
    • type, can be 'string' or 'password.' If type is 'password,' the input will be hidden as the user types it in
    • displayname, the name that will display above the authentication parameter in the UI

Example:

{
	"type": "basic",
	"authparams": {
		"username": {
			"type": "string",
			"displayname": "Username"
		},
		"password": {
			"type": "password",
			"displayname": "Password"
		},
		"instance_url": {
			"type": "string",
			"displayname": "Instance URL (w/o https)"
		}
	}
}

Later on, you can reference these parameters using the Mustache templates {{auth.username}} and {{auth.password}}.

Custom Authentication

Custom authentication allows you to define your own authentication parameters. Often, custom auth is used for key/secret authentication.

Template:

{
	"type": "custom",
	"authparams": {
		"": {
			"type": "",
			"displayname": ""
		}
	}
}

Fields:

  • type, must be set to "custom" for custom authentication
  • authparams, an object that contains the authentication parameters users will enter. For custom auth, you can define any authparams as you like
    • type, can be 'string' or 'password.' If type is 'password,' the input will be hidden as the user types it in
    • displayname, the name that will display above the authentication parameter in the UI

Example:

{
	"type": "custom",
	"authparams": {
		"apikey": {
			"type": "string",
			"displayname": "Username"
		},
		"secret": {
			"type": "password",
			"displayname": "Password"
		}
	}
}

No matter how you define your authentication schema, you'll be able to reference your authentication parameters later on using Mustache.

OAuth 1

Template:

{
	"type": "oauth",
	"version": "1.0",
	"authparams": {
		"": {
			"type": "",
			"displayname": ""
		}
	},
	"request_url": "",
	"access_url": "",
	"consumer_key": "",
	"consumer_secret": "",
	"callback": "{server}/app/oauth/(FILENAME)/authorize"
	"signature_method": "HMAC-SHA1",
	"nonce_size": null,
	"redirect": ""
}

Fields:

  • type, must be set to 'oauth' for OAuth 1
  • version, must be set to 1.0 for OAuth 1
  • authparams, an object that contains the authentication parameters users will enter. Only include authparams in the OAuth object if you require users to enter additional data before you can run the OAuth exchange, such as an instance URL. Each parameter defined in this object can be referenced as {{auth.(parameter key)}} using Mustache
    • type, can be 'string' or 'password.' If type is 'password,' the input will be hidden as the user types it in
    • displayname, the name that will display above the authentication parameter in the UI
  • request_url, the external URL where the token request will be made
  • access_url, the external URL where the request token will be exchanged for the access token
  • consumer_key, the access key obtained from the application
  • consumer_secret, the access secret obtained from the application
  • callback, the callback URL. Substitute the Channel's unique filename (the one you created when you first saved your Channel) for "FILENAME"
  • signature_method, must be 'HMAC-SHA1'
  • nonce_size, must be null
  • redirect, the redirect URL (may also be called the authorize URL).

The access token that results from the OAuth exchange can be referenced from inside each method and hashed into HTTP calls using the Mustache(#mustache-templates) template {{auth.access_token}}. After you have set up your authentication, all requests to the API will be automatically signed for you according to OAuth 1.0 protocol.

OAuth 2

Template:

{
	"type": "oauth",
	"version": "2.0",
	"authparams": {
		"": {
			"type": "",
			"displayname": ""
		}
	},
	"base_site": "",
	"authorize_path": "",
	"access_token_path": "",
	"access_token_name": "access_token",
	"refresh_token_name": "refresh_token",
	"callback": "{server}/app/oauth/(FILENAME)/callback",
	"client_id": "",
	"client_secret": "",_
	"appParams": {
		"one": {
			"state": true,
			"redirect_uri": "{server}/app/oauth/(FILENAME)/authorize"
		},
		"two": {
		"redirect_uri": "{server}/app/oauth/(FILENAME)/authorize"
		},
		"refresh": {
			"grant-type": ""
		}
	}
}

Fields:

  • type, must be set to 'oauth' for OAuth 2
  • version, must be set to 2.0 for OAuth 2
  • authparams, an object that contains the authentication parameters users will enter. Only include authparams in the OAuth object if you require users to enter additional data before you can run the OAuth exchange, such as an instance URL. Each parameter defined in this object can be referenced as {{auth.(parameter key)}} using Mustache
    • type, can be 'string' or 'password.' If type is 'password,' the input will be hidden as the user types it in
    • displayname, the name that will display above the authentication parameter in the UI
  • client_id, the client ID obtained from the application. Automatically appended to both legs of your authentication.
  • client_secret, the client secret obtained from the application. Automatically appended to both legs of your authentication.
  • base_site, the base URL
  • authorize_path, the URL (in addition to base_site) where users will be directed to give authorization
  • access_token_path, the URL (in addition to base_site) where the access token will be obtained
  • access_token_name, the key of the access token in the response
  • refresh_token_name(optional), the key of the refresh tokein in the response
  • callback, the callback URL. Substitute the Channel's unique filename (the one you created when you first saved your Channel) for "FILENAME"
  • appParams, an object containing the parameters for two-leg authentication
    • one, an object that contains the parameters required for the first leg of your authentication. The parameters included in the template are common parameters, but not all applications use the same fields.
      • state, a boolean that if true will include an unguessable state value in the OAuth exchange
      • client_id, the client ID provided when you registered your application
      • redirect_uri, the URL where users will be sent after authorization. Substitute the Channel's unique filename (the one you created when you first saved your Channel) for "FILENAME"
    • two, an object that contains the parameters required for the second leg of your authentication except for code, which is automatically passed to the second leg of the authentication exchange. The parameters included in the template are common parameters, but not all applications use the same fields.
      • client_id, the client ID provided when you registered your application
      • client_secret, the client secret provided when you registered your application
      • redirect_uri, the URL where users will be sent after authorization. Substitute the Channel's unique filename (the one you created when you first saved your Channel) for "FILENAME"
    • refresh (optional), an object that contains the parameters required to exchange a refresh token for a new auth_token if the token expires

The access token that results from the OAuth exchange can be referenced from inside each method and hashed into HTTP calls using the Mustache(#mustache-templates) template {{auth.access_token}}.

OAuth Redirect URLs

Some applications require you to set acceptable callback or redirect URLs when you register your application. You must add these redirect URLs to your application to be able to run OAuth from the Channel Builder, Beta Environment, and Production Environment:

For OAuth 1.0: https://betaapi.azuqua.com:443/app/oauth/(FILENAME)/callback

For OAuth 2.0: https://betaapi.azuqua.com:443/app/oauth/(FILENAME)/authorize

Substitute the Channel's unique filename (the one you created when you first saved your Channel) for "FILENAME".

Events, Actions, and Metadata

Introduction

Once you've set up Authentication, you can start adding Events and Actions to your Channel. Each Event and Action contains schemas that tell the UI how to build the visual elements of the card, and core code that actually makes the call to the API and handles the response. They are supported by metadata methods, which handle everything from building dynamic drop-down menus to branching and logic. Read more about each kind of method below.

Events

You can add a new Event to your Channel by selecting Events > Add New Method. Azuqua supports two kinds of Events, Polling and Webhook. Polling Events poll the API for new data at a regular interval determined by the user's plan. Webhook Events are triggered by the external App only when the desired Event occurs.

  • Webhook Events will only run when the corresponding webhook is activated. When the user turns on a FLO that starts with a Webhook Event, Azuqua will call an API endpoint to set up a webhook. Whenever the webhook is activated, the Event will run and trigger the rest of the FLO. When the FLO is turned off, the Event will tear down the webhook. Webhook Events are more efficient for both Azuqua and your API. If your API supports webhooks and allows all users to set up and tear down webhooks through the API, you should use Webhook Events in your Channel.
  • Polling Events run on a scheduled basis, checking the API at a regular interval for new records. New records are fetched using a single piece of saved data, called the "since" value. When the user turns on a FLO that starts with a Polling Event, Azuqua will call the API to get the current since value. Then, at a specified interval, the Event will call the API again and use the since value saved during the previous run to fetch new records. The engine will automatically execute the FLO once for every new record.

Webhook Events and Polling Events are set up very differently, since Webhook Events activate FLOs only when they are triggered, and Polling Events must often handle batches of records, and filter out the new records from the old. Read more about how to build each kind of Event:

Webhook Events

Webhook Events execute the FLO on-demand, only triggering FLOs when the desired Event occurs. To declare an Event as a webhook Event, click on the ellipses by the Event name, select "Configure Method," and check "This is a webhook."

A webhook Event has 5 sections:

  • Parameters, where you declare any parameters that will appear on the card
  • Output, where you declare the outputs that will appear on the card
  • Start, where you declare the steps that will setup the webhook when the user turns on their FLO
  • Core, where you declare the steps to handle incoming data from hte webhook
  • Stop, where you declare the steps that will tear down the webhook when the user turns off their FLO

The Start, Stop, and Core sections are all built using "bricks," the universal building-blocks that you can use to write programs for the Azuqua engine. Read more about bricks.

Polling Events

Polling Events will run on a recurring basis, based on the user's plan (typically 1-5 minutes). This means that unlike webhook Events, polling Events have to process collections of records and determine which records are new since the last time the Event ran. Most of the time a polling Event will output a collection of records, and the engine will automatically execute the FLO for each record in that collection.

A polling Event has 3 sections:

  • Parameters, where you declare any parameters for your method
  • Output, where you declare the output schema for your method
  • Core, where you declare the steps that will make the call to the API to fetch a collection of records, and handle the array that the API returns. As noted above, the Core section should return an array of records and the engine will automatically run the FLO for each record in that array.

Actions

When an Event triggers a FLO, each subsequent Action will run and execute the workflow that the user has designed. By adding Actions from different Channels to their FLO and dragging data between them, Azuqua users can connect any of APIs that Azuqua supports.

Actions have 4 sections:

  • Params, where you declare any parameters that will appear on the card
  • Input, where you declare any inputs that will appear on the card
  • Output, where you declare any outputs that will appear on the card
  • Core, where you declare the steps that will make the call to the API to perform the Action and handle any data that the API returns.

In general, Actions take in data, use it to run a call to the API, then shape the resulting object into the declared output format. They do not return collections, just single objects.

Metadata Methods

Metadata methods are helper methods that supplement the Events and Actions. Some uses of Metadata methods include:

  • Generating the arrays that populate dynamic dropdowns
  • Building dynamic input and output schemas that include users' custom fields
  • Containing code that will be re-used in several methods
  • Executing branches from main methods
  • Executing complex loops
  • Error handling

Metadata methods only have one section, the Core array where you declare the bricks that execute the method. Since they do not appear in the UI, they do not require parameters, input, and output schemas. However, when you call Metadata methods you can pass certain values to the method as inputs.

How you send inputs to Metadata methods depends on the context where you are calling the Metadata method. The Call brick declares input values a little differently than dynamic drop-downs or dynamic inputs do. However you declare them, you can reference inputs from inside the Metadata method the same way you would any other input: using the mustache tag {{input.NAME}}.

Defining Parameters

In the parameters section, you define any information (in addition to authentication) that the user will need to enter for the Event or Action to make the call to the API.

It is important to note the difference between parameters and inputs: while inputs can accept data that is dragged and dropped from an earlier card, parameters do not accept any data from previous cards. In some cases, you would make data a parameter (and not a regular input) when you need more information from the user to be able to show them the correct input or output fields. For example, if your application allows custom record types you may need to know which type of record a user wants to work with to be able to show them the correct fields.

Note, you should not name your parameter "module." You just can't.

Template:

[
	{
		"": {
			"type": "",
			"displayname": ""
		}, {
		"": {
			"type": "",
			"displayname": ""
		}
	}
]

Fields:

  • type, the type of your parameter (choices: 'option,' 'string'). Read more about each kind below
  • displayname, the name that will display above the parameter on the card

String Parameters

These parameters require user to type in data to a field. They are less common than dropdown parameters. In most cases, it is better to make an input that users can drag data into than a string parameter.

However, there are some string parameters on Events. One example is the Twitter "Monitor Keyword" event, which requires users to type in the keyword they would like to monitor as a parameter.

Template:

"": {
	"type": "string",
	"displayname": "",
	"description": ""
}

Example:

"search": {
	"type": "string",
	"displayname": "Keyword"
}

Fields:

  • type the type of the parameter. Must be set to "string" for a dropdown
  • displayname, the name that will display above the parameter on the card

Static Dropdowns

Template:

"": {
	"type": "option",
	"displayname": "",
	"choices": [
		"",
		"",
		""
	]
}

Example:

"radius": {
	"type": "option",
	"displayname": "Radius (Miles)",
	"choices": [
		".125",
		".25",
		".5",
		"1",
		"5",
		"10",
		"15",
		"20",
		"25",
		"35",
		"50",
		"100",
		"150"
	]
}

Fields:

  • type, the type of the parameter. Must be set to "option" for a dropdown
  • displayname, the name that will display above the parameter on the card
  • choices, an array of the items that will appear in your dropdown menu.

Dynamic Dropdowns

Dynamic dropdowns call a metadata method to generate a dropdown list for the user to choose options from. It is important that the metadata method called from a dynamic dropdown parameter returns only an array, with any surrounding metadata removed. Using the objects in this array, you can also specify the path to a displayname (key) from each object that will be shown to the user, while a different parameter (value) is passed on as the value of this parameter. Later on, you can reference this value using mustache(#mustache-templates) as {{params.(paramName)}}.

When you are building a card with a dynamically-generated dropdown, Metadata methods are used to make a call to the API in order to fetch an array of options for the user to choose from. If necessary, they may also include steps to filter the information to narrow the user's choices, or otherwise manipulate the data into a format necessary to render a dynamic dropdown(#dynamic-dropdown). You can also link multiple methods together to run dependent dynamic dropdowns.

Template:

[
	{
		"": {		
			"type": "option",
			"displayname": "",
			"dependsOn": [
				""
			],
			"drives": [
				""
			],
			"choices": [],
			"lookup": {
				"channel": "",
				"operation": "",
				"key": "",
				"value": ""
			}
		}
	}
]

Fields:

  • type the type of the parameter. Must be set to "option" for a dropdown
  • displayname, the name that will display above the parameter on the card
  • dependsON, an array containing the parameter(s) that this parameter depends on, meaning the user must choose a value for the dependsOn parameter before the current parameter can run the method called by lookup. For more information, see the note on dependent dynamic dropdowns(#dependent-dynamic-dropdowns).
  • drives, an array containing the parameter(s) that this parameter drives, meaning that the user must choose a value for the current parameter before the drives parameter can run the metadata method called by its lookup object. For more information, see the note on dependent dynamic dropdowns(#dependent-dynamic-dropdowns).
  • choices, an array of the items that will appear in your dropdown menu. For dynamic dropdown, choices should be left empty
  • lookup, the information that will be used to call the metadata method and dynamically generate the dropdown
    • channel, the name of the current channel (lowercase)
    • operation, the name of the metadata method you would like to call to generate the array. The output of this method should be an array of objects.
    • key, the path to the display name that will appear in the dropdown relative to the index of the current item. You can use dot notation to point to the display name you would like to use.
    • value, the value that will be passed to the bricks array as {{params.(paramName)}}. This can be different from key, allowing you to use a user-friendly display name in your dropdown while passing along an ID as the value of this parameter.

Example:

"sheet": {
	"type": "option",
	"displayname": "Your smartsheet sheets",
	"choices": [],
	"lookup": {
		"channel": "smartsheet",
		"operation": "getSheets",
		"key": "name",
		"value": "id"
	}
}

Dependent Dynamic Dropdowns

To define multiple dropdowns that are dependent on one another (i.e. a parameter uses the value of another parameter to run its metadata method) you must make sure that the drives and dependsOn attributes are correct for the UI to correctly render the card and ensure that the second parameter will not run its metadata method until the user has chosen a value for the first. For example, in the Trello "New Card in List", the user must fist choose the board they would like to monitor from a dynamically generated dropdown (this parameter is called board in the Channel definition file). Then, the value of this parameter is used to dynamically generate a dropdown of lists in that board (this parameter is called list).

The dependsOn array in the list parameter contains the value "board" and the drives array in board parameter contains the value "list". Because of this dependency, the value of board is implicitly passed as an input to the metadata method getBoardLists. getBoardLists can use the mustache template {{input.board}} to reference the value of the board parameter.

Defining Inputs

The input section of a method defines the input fields on your card and provides a schema to build the object that will be passed to the bricks array.

The object defined by the input section must follow this structure:

{
	"Header": {
		"Field": "Value",
		"Field": "Value"
	},
	"Header": {
		"Field": "Value",
		"Field": "Value"
	}
}

There must be at least one "Header" object that wraps the input fields, and at least one input field inside each of those objects.

Static Inputs

Most input sections are static--the user will always see the same input fields on this card, regardless of their account data. For example, the "Post Tweet" Action from Twitter will always have the same input fields.

Template:

{
	"extensible" : false
	"attributes" : [
		{
			"name": "",
			"attributes": [
				{
					"name": "",
					"type": "sring"
				}
			]
		}
	]
}

Fields:

  • extensible, a boolean that if 'true' will allow users to add their own custom input fields. This should only be used if you are unable to dynamically generate users' custom fields using a metadata method
    • attributes, an array that contains the header objects
      • name, the name of the header object. This will be displayed on the card
      • attributes, an array that contains the input fields
        • name, the name of the input field. This will be displayed on the card
        • type, must be 'string'

Example:

{
	"extensible": false, 
	"attributes": [
		{ 
			"name": "User",
			"attributes": [
				{
					"name": "firstName",
					"type": "string"
				},
				{
					"name": "lastName",
					"type": "string"
				},
				{
					"name": "email",
					"type": "string"
				}
			]
		}, 
		{
			"name": "Company",
			"attributes": [
				{
					"name": "name",
					"type": "string"
				}, 
				{
						"name": "revenue",
						"type": "string"
				}
			]
		}
	]
}

This input schema would generate an input object that looks like this, after the user has entered data:

{
	"User": { 
		"firstName": "Demo", 
		"lastName": "Ersatzson",
		"email": "demo@unicorn.com"
	},
	"Company": {
		"name": "Unicorn Industries",
		"revenue": "1000000000"
	}
}

Dynamic Inputs

You can also take the user's account information and use it to dynamically generate inputs. This should only be done if the application supports custom fields for the record type you are dealing with. You can use a metadata method to call the API and fetch the users' custom fields, and then build them into the card.

Template:

{
	"extensible": false,
	"attributes": [
		{
			"name": "Normal Fields",
			"attributes": [
				{
					"name": "Field1",
					"type": "string"
				},
				{
					"name": "Field2",
					"type": "string"
				}
			]
		},
		{
			"name": "Custom Fields",
			"attributes": [
				{
				"metadata": "",
				"config": "",
				"data": {}
				}
			]
		}
	]
}

Fields:

  • metadata, the name of the metadata method you would like to call

The metadata method should call the API to obtain data about the user's custom fields, then return an array of strings. Each string in that array will be turned into a field.

Example:

{
	"extensible": false,
	"attributes": [
		{
			"name": "Ticket",
			"attributes": [
			{
				"name": "Name",
				"type": "string"
			},
			{
				"name": "Description",
				"type": "string"
			}
		]
	},
	{
		"name": "Custom Fields",
			"attributes": [
				{
					"metadata": "getTicketFields",
					"config": "",
					"data": {}
				}
			]
		}
	]
}

The above example would call a metadata method called getTicketFields. getTicketFields would then run a call to the API, and render the results into an array.

Then, in the Core array, you can use bricks to manipulate the input data before passing the data the API.

Defining Outputs

The output section defines the output object that the card expects from the bricks(#bricks). This information is used to generate the card at design time, so the user can drag their data from one card to another without reference to the API underlying the card. If the result of brick steps is not the same as the object declared in the output schema, the data will not be passed to any following cards.

The object defined by the output section must follow this structure:

{
	"Header": {
		"Field": "Value",
		"Field": "Value"
	},
	"Header": {
		"Field": "Value",
		"Field": "Value"
	}
}

There must be at least one "Header" object that wraps the output fields, and at least one output field inside each of those objects.

Static Outputs

Most output objects are static--the user will see the same output fields on the card, regardless of their account data. For example, no matter the user, the "New Card in List" Event from Trello will always have the same output fields.

Template:

{
	"extensible" : false
	"attributes" : [
		{
			"name": "",
			"attributes": [
				{
				"name": "",
				"type": "sring"
				}
			]
		}
	]
}

Fields:

  • extensible, a boolean that if 'true' will allow users to add their own custom output fields. This should only be used if you are unable to dynamically generate users' custom fields using a metadata method
  • attributes, an array that contains the header objects
    • name, the name of the header object. This will be displayed on the card
    • attributes, an array that contains the output objects
      • name, the name of the output field. This will be displayed on the card
      • type, must be 'string'

Example:

{
	"extensible": false, 
	"attributes": [
		{ 
			"name": "Post",
			"attributes": [
				{
				"name": "Text",
				"type": "string"
				}, 
				{
				"name": "URL",
				"type": "string"
				}, 
				{
				"name": "Reblogs",
				"type": "string"
				}
			]
		}, 
		{
			"name": "Author",
			"attributes": [
				{
					"name": "Username",
					"type": "string"
				}, 
				{
					"name": "Followers",
					"type": "string"           
				}
			]
		}
	]
}

This output schema would expect an output object from the bricks section that looks like this:

	{
	"Post": { 
	"Text": "Bagels are just donuts without the happiness.", 
	"URL": "www.donutfans.com/post/1029384756",
	"Reblogs": "47"
	},
	"Author": {
	"Username": "KrullerKing",
	"Followers": "189"
	}
	}

Dynamic Outputs

You can also take the user's account information and use it to dynamically generate outputs. This should only be done if the application supports custom fields for the record type you are dealing with. You can use a metadata method to call the API and fetch the users' custom fields, and then build them into the card.

Template:

{
	"extensible": false,
	"attributes": [
		{
			"name": "Normal Fields",
			"attributes": [
				{
					"name": "Field1",
					"type": "string"
				},
				{
					"name": "Field2",
					"type": "string"
				}
			]
		},
		{
			"name": "Custom Fields",
			"attributes": [
				{
					"metadata": "",
					"config": "",
					"data": {}
				}
			]
		}
	]
}

Fields:

  • metadata, the name of the metadata method you would like to call

The metadata method should call the API to obtain data about the user's custom fields, then return an array of strings. Each string in that array will be turned into a field.

Example:

{
	"extensible": false,
	"attributes": [
		{
			"name": "User",
			"attributes": [
				{
					"name": "Full Name",
					"type": "string"
				},
				{
					"name": "Email",
					"type": "string"
				},
				{
					"name": "ID",
					"type": "string"
				}
			]
		},
		{
			"name": "Custom Fields",
			"attributes": [
				{
					"metadata": "getTicketFields",
					"config": "",
					"data": {}
				}
			]
		}
	]
}

The above example would call a metadata method called getTicketFields. getTicketFields would then run a call to the API, and render the results into an Array. The result of the Core array must be the same shape as the output object defined in your schema. In the example above, if the user had two custom ticket fields, "plan_type" and "csm_owner", the output object produced by the Core array should look like this:

{
	"User": {
		"Full Name": "Pseudo Ersatzson",
		"Email": "pseudo@illusory.com",
		"ID": "0112358132134"
	},
	"Custom Fields": {
		"plan_type": "Professional",
		"csm_owner": "John Smith"
	}
}

Brick Basics

Introduction

You've set up your authentication and visual elements, but now you actually need to connect your Channel to an API. You can do this using Azuqua's pre-built library of declarative building blocks, known as bricks. Basically, bricks lay out a set of steps that your Channel should execute at run-time, determining how user data is turned into API requests, and how API data is in turn rendered into a user-friendly format.

In the "Core" section of your method (or, in the case of webhooks, the "Start" and "Stop" sections) you can declare a chain of bricks that will handle everything from HTTP requests to JSON parsing and data manipulation. In this section, learn more about how bricks work, and some basic tricks to programming with them. Then, check out the Bricks Library to get a deep dive into each brick.

Implicit vs. Explicit Data Passing

Bricks are executed subsequently, and each brick has access to the data before it. However, this doesn't work the same way in every brick. Some bricks accept data implicitly, meaning that it automatically operates on the data produced by the previous bricks. Other bricks require you to explicitly declare data using Mustache. When you're looking through the bricks library, look carefully at each brick to determine if it works with data implicitly or explicitly--some bricks actually use both kinds of data declaration.

Using Mustache

Azuqua uses Mustache templates to hash data from different parts of the method into a brick. Using mustache, you can reference the parameters, inputs, or data from another brick.

For example, if you're building an Action card that creates a new resource (lin this example, let's say it's a new task), you'll probably need to take information that the user gives you and then send it to the API.

When you set up your inputs you may have included a field for the user to put in the name of their task:

{
	"name": "Task",
	"attributes": [
		{
		"name" : "Task Name",
		"type": "string"
		}
	]
}

The resulting input object will look something like this:

	{
		"Task: {
			"Task Name": ""
		}
	}

Later, you'll want to hash this data into the body of your API request. This is where mustache comes in handy:

"body": { 
	"task_name": "{{input.Task.Task Name}}"
}

At runtime, the mustache will be swapped out for the data that the user enters or drags over from another card.

Universal Mustache Tags

Inside of Azuqua, there are certain universal mustache tags you can use to refer to different parts of your method:

  • {{params.parameterName}}, where parameterName is the name of your parameter object in the Parameters section of your method. Use this tag to refer to the value of any parameters that the user enters.
  • {{input.Header.Field Name}}, where Header is the "name" value in your header object, and Field Name is the "name" value of the field you would like to reference. Use this tag to refer to any data that the users enters as an input. You can also use it to reference inputs inside of Metadata methods that have been explicitly declared from the calling brick or schema.
  • {{prevData.Data}}, where Data is the location of the data you would like to access from the output of the previous brick. Use this tag to pull data into a brick from the brick right before it.
  • {{allData.0.Data}}, where 0 is the index of the brick you would like to reference, and Data is the location of the data you would like to access from the output of that brick. Use this tag to pull data into a brick from any previous brick.

Mustache Hacks

Mustache has a couple of built-in features that can make building Channels a lot easier, if you know how to use them.

Escaping/Unescaping

By default, mustache renders your tags as escaped HTML. Most of the time, this isn't a problem, since HTML inputs aren't common in Azuqua. But If you want to leave your HTML unescaped you can use {{{triple brackets}}} instead of {{double ones}}. Often, if your string is rendering with unexpected characters, the problem is in the number of brackets you are using.

In some cases, you should avoid mustache escaping altogether by using the unstringify utility. You should always use unstringify if you are attempting to mustache an object, or if the string you are rendering may contain the "" character (which will interfere with mustache's rendering capabilities).

Conditional templates

There are lots of cases where you may want to only include mustache templates if certain conditions are met. The most common is when you are updating a resource. If the user doesn't enter data for a certain field, it will be passed to the bricks as an empty string. However, you don't want to overwrite data by sending an empty string to the API.

For example, if we're updating a contact, the input section of the card might look something like this:

{
	 "name": "Contact",
	  "attributes": [
		{
			"name": "Contact Name",
			"type": "string"
		},
		{
			"name": "Contact Email",
			"type": "string"
		}
	]
}

...and the body of the API request looks like this:

"body": {
	"contact_name": "{{input.Contact.Contact Name}}",
	"contact_email": "{{input.Contact.Contact Email}}"
}

However, the user may not want to update all of these fields when they actually run the card. When the FLO actually runs, the resulting input object built by the card might look like this:

{
	"Contact": {
		"Contact Name": ""
		"Contact Email": "demo@azuqua.com"
	}
}

Then, if the empty data is mustached into the body of the call to the API, the API would be sent an empty string:

"body": {
	"contact_name": "",
	"contact_email": "demo@azuqua.com"
}

This would update the email like the user wants, but overwrite the existing contact_name with an empty string. Instead, you can use mustache's built-in logic to check if the fields exist before templating them into the API request.

"body": {
	"{{#input.Contact.Contact Name}}contact_name{{/input.Contact.Contact Name}}": "{{input.Contact.Contact Name}}",
	"{{#input.Contact.Contact Email}}contact_email{{/input.Contact.Contact Email}}": "{{input.Contact.Contact Email}}"
}

This time, when the user doesn't enter a value for a field, mustache will check to see if the field exists before including it in the body of the request to the API. Now, the request to the API will look something like this:

"body": {
	"": "",
	"contact_email": "demo.azuqua.com"
}

The contact_name field isn't included, since the user didn't enter a value. That way, the email will be updated, and the existing contact name will be left alone.

As you've probably figured out by now, Mustache is actually evaluating an expression--that's what the # character is for. If the expression evaluates to true, then mustache will include the value between the two sets of brackets. You can use the ^ character in place of the # character to run a function that will only include the value between the brackets if the expression evaluates to false.

Utilities

Azuqua offers several built-in utilities in addition to mustache to manipulate your data. They are useful when you want to get around Mustache's string-conversion, or to correctly mustache values into URLs. Utilities use the Mustache "sections" template to run transformations on data. To call a utility from a brick, use the following template:

{{#utils.name}}path{{/utils.name}}

Base64

Base64 allows you to Base64 encode a piece of data before hashing it into a url.

Example:

{{utils.base64}}input.url{{/utils.base64}}

Unstringify

Unstringify allows you to use Mustache to hash objects or arrays into a brick. For example, if the response from an API includes an array of tags attached to a record, you can use Unstringify to pass the array on as a list to another brick.

Example:

{{#utils.unstringify}}result.tags{{/utils.unstringify}}

Hash

Hash allows you to create a cryptographic hash from mustached data. Hash takes in three pieces: the data you want to hash, the algorithm you would like to use, and the digest. Hash accepts any valid algorithm supported by OpenSSL (sha1, md5, sha256, sha512, etc.). Supported digest values are base64, hex, and binary.

Example:

{{#utils.hash}}{{auth.client_secret}}, sha256, hex{{/utils.hash}}

Bricks Library

Branch

The Branch brick lets you put conditional logic inside of your method and call metadata methods if certain conditions are met. It can compare numbers, booleans, and strings. Strings will be compared by the ASCII value associated with each character in the string. The output of the Branch brick is then set to the output of the metadata method that is called.

Template:

{
  "brick" : "branch",
  "config" : {
    "conditions": [{
      "expression" : {
        "operator": "",
        "A": "",
        "B": ""
      },
      "method" : "",
      "input" : {}
    },{
      "default" : true,
      "method" : "",
      "input" : {}
    }]
  }
}

Fields:

  • conditions, an array that contains an object defining each condition you would like to check for
    • expression, an object that contains the expression you would like to
      • operator, the operator of the expression. Options: <, >, <=, >=, ==, !=
      • A, the first variable in the expression
      • B, the second variable in the expression
    • default, a boolean that flags this object as the default condition. Defaults to false. All condition objects will have either the default field or the expression field
    • method, the name of the metadata method to call if the expression is true, or if this condition is the default and all other conditions are false
    • input, an object containing any values you would like to pass to the metadata method as inputs. Inside the metadata method, you can reference these values as if they were normal inputs using mustache (e.g. {input.fieldName})

Example:

This branch checks for basic value comparison:

{
  "brick": "branch",
  "config": {
		"conditions": [
			{
				"expression": 
				  "operator": "==",
				  "A": "{{input.data.a}}",
				  "B": "{{input.data.b}}"
				},
				"method": "caseOne",
				"input": { 
					"data": "{{#utils.unstringify}}input.data{{/utils.unstringify}}",
				}
			},
			{
				"expression": {
					"operator": ">",
					"A": "{{input.data.a}}",
					"B": "{{input.data.b}}"
				},
				"method": "caseTwo",
				"input": { 
					"data": "{{#utils.unstringify}}input.data{{/utils.unstringify}}",
				}
			},
			{
				"default": true,
				"method": "caseThree",
				"input": { 
					"data": "{{#utils.unstringify}}input.data{{/utils.unstringify}}",
				}
			}
		]
	}
}

Each condition calls a seperate metadata method. The steps of the metadata method called by the branch brick will be logged in the console, and the result of the metadata method will be set as the result of the branch brick.

Call

The Call brick lets you call a metadata method from inside another method. You can use this brick inside the Collections: Map brick(#map) to run a metadata method on each item of an array.

Template:

{
	"brick": "call",
	"config": {
		"method": "",
		"input": {
			"": ""
		}
	}
}

Fields:

  • method, the name of the metadata method you would like to run
  • input, an object containing any values you would like to pass to the metadata method as inputs. Inside the metadata method, you can reference these values as if they were normal inputs using mustache (e.g. {input.fieldName})

Example:

{
	"brick": "call",
	"config": {
		"method": "filterSocial",
		"input": {
			"profileType": "twitter",
			"profileData": "{{#utils.unstringify}}prevData{{/utils.unstringify}}"
		}
	}
}

Collections

The Collections brick lets you run a series of operations on arrays of data. This brick is especially useful when you are building Events and querying an API endpoint for a list of new results. There are two other bricks that work with arrays but are not part of the collections operations: Massage and Sort. You can use Massage to transform objects in arrays, and you can use Sort to sort arrays. All other collections operations are handled by the collections brick.

The Collections brick takes in an array (to use collections, prevData must be an array) and will return an array, modified according to the specified operation. There are five operations you can perform with collections: Map, Filter, Flatten, Limit, and Zip. Read more about each operation below:

Map

The map operation runs another brick on each element of an array, replacing that element with the result of the brick operation.

Template:

{
	"brick": "collections",
	"config": {
		"operation": "map",
		"item": "",
		"call": { 
			"brick": "",
			"config": {}
		}
	}
}

Fields:

  • operation, must be set to "map"
  • item, allows you to declare an alias to reference the current item in the array. You can use this name inside the nested brick to refer to a property of the current item using Mustache (see example below)
  • call, an object containing the full brick you would like to call.

Example:

{
	"brick": "collections",
	"config": {
		"operation": "map",
		"item": "record",
		"call": {
			"brick": "http",
			"config": {
				"method": "GET",
				"url": "https://www.fakeapp.com/api/readRecord/{{record.id}}",
				"headers": { 
					"Authorization": "Bearer {{auth.access_token}}"
				}
			}
		}
	}
}

Filter

The filter operation searches through an array of similar objects and uses a comparison to filter out objects based on the value at a certain path.

Template:

{
	"brick": "collections",
	"config": {
		"operation": "filter",
		"item": "",
		"operator": "",
		"path": "",
		"compareTo": ""
	}
}

Fields:

  • operation, must be set to 'filter
  • item, allows you to declare an alias to reference the current item in the array. You can use this alias to refer to properties of the current item inside of the compareTo parameter. For example, if you set "item": "record" you can reference that alias using mustache: "compareTo": "{{record.attribute}}". However, you can not use mustache inside of the path parameter. path is always releative to the current item.
  • operator, the operator you would like to use to compare the value at "path" and the "compareTo" value. You can compare both strings and numbers (Valid inputs: ==, !=, >=, <=, >, <)
  • path, the path of the value you want to compare. This path is relative to the index of the current item in the array, so you do not need to use Mustache to explicitly call prevData.
  • compareTo, the value you would like to compare the value found at path to. You can set a static value for compareTo or use mustache to hash in an input, parameter, or value from a previous brick

Example:

{
	"brick": "collections",
	"config": {
		"operation": "filter",
		"operator": "==",
		"path": "timeUpdated",
		"compareTo": "{{{since}}}"
	}
}

Flatten

The flatten operation flattens a multi-dimensional array into a one-dimensional array.

Template:

{
	"brick": "collections",
	"config": {
		"operation": "flatten"
	}
}

Fields:

  • operation, must be set to "flatten"

Limit

The limit operation limits the length of an array by slicing off any items that exceed the set length.

Template:

{
	"brick": "collections",
	"config": {
		"operation": "limit",
		"length": ""
	}
}

Fields:

  • operation, must be set to "limit"
  • length, an integer representing the length (number of items) in the resulting array

Zip

The limit operation takes in two explicitly called arrays of the same lenth and zips them together into a single, two-dimensional array.

Template:

{
	"brick": "collections",
	"config": {
		"operation": "zip",
		"array1": "",
		"array2": ""
	}
}

Fields:

  • operation, must be set to "zip"
  • array1, the mustache reference to the first array
  • array2, the mustahce reference to the second array

Example:

{
	"brick": "collections",
	"config": {
		"operation": "zip",
		"array1": "{{prevData}}",
		"array2": "{{allData.0}}"
	}
}

Date

The Date brick can convert ISO8601 timestamps into Unix, Unix into ISO8601, or get the current timestamp in either format.

Template:

{
	"brick" : "date",
	"config": {
		"date" : "",
		"zone" : "",
		"format" : ""
	}
}

Fields:

  • date, the date you would like to convert. Accepts full or partial ISO8601 strings, or unix timestamps. Leave empty if you would like to get the current time.
  • zone, the timezone you would like to use. Only use this field if you are obtaining the current time and format is equal to iso8601. Acceptable timezones are documented here: http://momentjs.com/timezone/
  • format, the desired output format. Options are x for Unix time and iso8601 for ISO8601 time. If you choose iso8601 and do not enter a value for zone, you will be given the current UTC time.

Example:

{
  "brick": "date",
  "config": {
		"date" : "{{prevData.metadata.date}}",
		"zone" : "",
		"format" : "x"
	}
}

The brick shown above will convert an ISO8601 string into Unix time, assuming that the value at prevData.metadata.date is a full or partial ISO8601 string. For example, if prevData.metadata.date is 2015-12-15T18:13:29+08:00, the output of the Date brick will be 1450174409000.

Dateslice

The Dateslice brick takes in a list of records in descending date order, and slices the list at a certain date. Only records that have occured since that date will be passed to the next brick. Dateslice should only be used if the API does not make it possible to fetch new records by passing the date or ID as a parameter in your request.

Template:

{
	"brick": "dateslice",
	"config": { 
		"date": "{{since}}",
		"path": ""
	}
}

Fields:

  • date, the date you would like to slice the list at. Dateslice will return only records with a date value greater than this value. Usually, the Dateslice brick is paired with the Since brick(#since) and the date value is set to {{{since}}}.
  • path, the relative path where the date is stored in each record. You do not need to use mustache to declare path.

Example:

In this example, the HTTP brick returns a list of all records in descending order by the date that the record was created:

Brick 1: HTTP

{
	"brick": "http",
	"config": {
		"method": "GET",
		"url": "http://api.fakeapp.com/records/list"
	}
}

Data:

[
	{
		"recordId": "7",
		"dateCreated": "1450414800"
	},
	{
		"recordId": "3",
		"dateCreated" : "1116478800"
	},
	{
		"recordId": "2",
		"dateCreated": "1021525200"
	},
	{
		"recordId": "1",
		"dateCreated": "927100800"
	},
	{
		"recordId": "6",
		"dateCreated": "422686800"
	},
	{
		"recordId": "5",
		"dateCreated": "327733200"
	},
	{
		"recordId": "4",
		"dateCreated": "233384400"
	}
]

Then, the Dateslice brick iterates through the array until it finds a record with a value at the path "dateCreated" that is equal to the last {{{since}}} value that was saved to the engine. Next, it slices the array before that record, so only the records that have been created since that record are passed to the next brick. If the last {{{since}}} value that was saved to the engine for this FLO was 1116478800, then the resulting array would look like this:

[	
	{
		"recordId": "7",
		"dateCreated": "1450414800"
	}
]	

The method then saves the timestamp of the most recent record using the Since brick:

{
	"brick": "since",
	"config": {
		"path": "{{prevData.0.dateCreated}}",
	}
}

The next time the FLO is run, this value will be used by the Dateslice brick to slice the next set of records.

Hash

The Hash brick uses a key (that can either be hard-coded or mustache) to look up a value in a hash.

Template:

{
	"brick": "hash",
	"config": {
		"key": "",
		"hash": {
			"": ""
		}
	}
}

Fields:

  • key, the key you would like to look up in the hash
  • hash, an object containing the hash (a set of key/value pairs) you would like to reference

Example:

{
	"brick": "hash",
	"config": {
		"key": "{{prevData.path.to.key}}",
		"hash": { 
			"a": "hello",
			"b": "world"
		}
	}
}

HTTP

The HTTP brick is the core brick. This brick implements the HTTP protocol, which is used in every REST API.

Template:

{
	"brick": "http",
	"config": {
		"method": "",
		"format": "",
		"auth": true,
		"url": "{{auth.subdomain}}/api/search",
		"query": {},                  
		"headers": {},                  
		"body": {}
	},
	"handler" : 
		"method" : "",
		"config" : {}
	}
}

Fields:

  • config, an object that contains the configuration for the brick
    • method, the HTTP method you would like to use in your request. Takes any valid HTTP verb.
    • format, the format of your request. Automatically sets the Content-Type header of your request for you. Options: 'urlencoded', 'json', 'xml', 'plain'.
    • auth, an optional parameter that defaults to true. If your authentication scheme is OAuth, Azuqua will automatically sign your HTTP requests according to the OAuth standard. If you would not like Azuqua to sign your requests, set auth to false.
    • url, the url of your HTTP request. You can use mustache in your url, but everything in this field will be automatically URL encoded.
    • query, an optional field that appends any query parameters to the url. These parameters will not be urlencoded. Accepts either an object or a string.
    • headers, an object containing the headers you would like appended to your request as key/value pairs
    • body, an object containing the body parameters you would like sent over in your request as key/value pairs
  • handler, an optional object that contains information about how this brick should handle error responses. The header attribute should only be used when the method should execute even if there is an API error. Read more about error handling(#using-metadata-to-handle-http-errors).
    • method, the name of the metadata method that should be called in the case of an error response
    • config, an object containing any additional parameters that should be sent to the metadata method as inputs. Parameters defined in the config can be accessed from inside the metadata method using the mustache template {{input.(parameter name)}}

Example:

{  
	"brick": "http",
	"config": {
		"method": "POST",
		"url": "https://api.trello.com/1/lists/{{params.list}}/cards",
		"auth": false,
		"query": {
			"key": "{{auth.consumer_key}}",
			"token": "{{auth.access_token}}"
		},
		"body": {
			"name": "{{input.card.name}}",
			"desc": "{{input.card.description}}",
			"labels": "{{input.card.labels}}"
		}
	}
}

Using Metadata to handle HTTP errors

In some cases, if your API returns an error you would want your Event or Action to return that error, and stop execution of the FLO. However, most of the time your method should be able to handle API errors so the rest of the FLO can execute even if this card fails.

Since later bricks may take a dependency on the data that returns from the API, if an error occurs in your HTTP brick you can call a metadata method to handle the error. This method should build an object that will enable any subsequent bricks to execute as if the API had returned data instead of throwing an error--in most cases, just returning an empty object will suffice. The result of this error handling method will then be returned to the parent method as the output of the HTTP brick that called it, and the next brick in the parent method will run on the result of the error handling method.

Example:

In this example, the metadata method is called from the Action "Search Contacts." The API will return an error if no contacts matching the query are found.

Method: Search Contacts

Step 1: HTTP Brick

{
	"brick": "http",
	"config": {
		"method": "GET",
		"url": "https://api.exampleapp.com/v2/records/search",
		"query": "firstName={{input.Contact.First Name}}&lastName={{input.Contact.Last Name}}"
	},
	"handler": {
		"method": "errorHandler",
		"config": {}
	}
}

Step 2: Render Brick

{
	"brick": "render",
	"config": {
		"output": {
			"Contact": {
				"Email": "{{prevData.contactData.email}}",
				"Company": "{{prevData.company.name}}",
				"Title": "{{prevData.company.title}}"
			}
		}
	}
}

The mustache in the Render Brick takes a dependency on the API response. If the response from the API does not JSON (e.g. an error message), the Render Brick will not be able to run and the method will fail, stopping the FLO. To prevent this problem, the HTTP brick calls the metadata method "errorHandler."

Method: errorHandler

Step 1: Render Brick

{
	"brick": "render",
	"config": {
		"output": {}
	}
}

Since errorHandler returns an empty JSON object to the parent method, the Render brick in "Search Contacts" will now be able to execute. In the event of an error, the output of "Search Contacts" will still be a JSON object, but instead of values it will contain empty strings:

{ 
	"Contact": {
		"Email": "",
		"Company": "",
		"Title": ""
	}
}

The FLO will be able to continue, using these empty strings as output data.

Massage

Massage changes the structure of JSON, allowing you to take the output of an API and transform it into data that the front-end can use.

Massage is similar to "render," but is more suitable for complex objects and arrays.

Template (object):

{
	"brick": "massage",
	"config": {
		"schema": {
			"type": "object",
			"properties": {
				"" : {
					"type": "object",
					"properties": {
						"": {
							"type": "string",
							"path": ""
						}
					}
				}
			}
		}	
	}
}
}

Template (array):

{
	"brick": "massage",
	"config": {
		"schema": {
			"type": "array",
			"items": {
				"type": "object"
				"properties": {
					"": {
						"type": "object",
						"properties": {
							"": {
								"type": "string",
								"path": ""
							}
						}
					}
				}
			}
		}
	}
}

Fields:

  • schema: an object that defines your JSON schema
    • type: the type of the data you are working with. Accepts 'object','array', or 'string'. If type is set to array, Massage expects prevData to be an array and will iterate through that array, applying the schema to every object.
    • properties: if type is 'object', properties describes the object you would like to build. properties can contain fields or other nested objects.
    • items: if type is 'array', items is an object that defines the schema that will be applied to each item in the array

Example:

{
	"brick": "massage",
	"config": {
		"schema": {
			"type": "array",
			"items": {
				"type": "object"        
				"properties": {         
					"User": { 
						"type": "object",
						"properties": {
							"Email": {
								"type": "string",
								"path": "email"
							},
							"Full Name": 
								"type": "string",
								"path": "name.fullname"
							},
							"Company": 
								"type": "string",
								"path": "company.name"
							}
						}
					}
				}	
			}   
		}
	}
}

This example will take data that looks like this:

[
	{
		"email": "demo@initech.com",
		"name": { 
			"firstname" : "Pseudo",
			"lastname":  "Ersatzson",
			"fullname": "Demo Ersatzson"
		},
		"company": 
			"name": "Initech",
			"city": "Austin"
			"state": "TX"
		}
	}, 
	{
		"email" : "urist@bluesun.cn",
		"name": {
			"firstname" : "Urist",
			"lastname":  "McFaker",
			"fullname": "Urist McFaker"
		},
		"company": {
			"name": "Blue Sun Corporation",
			"city": "Mountain View"
			"state": "CA"
		}
	}
]

And transform it into an array that looks like this:

[
	{
		"User": {
			"Email" : "demo@initech.com",
			"Full Name" : "Demo Ersatzon"
			"Company": "Initech"
		}
	},
	{
		"User": {
			"Email": "urist@bluesun.com",
			"Full Name": "Urist McFaker",
			"Company": "Blue Sun"
		}
	}
] 

Note how Massage applied the schema to each element in the array.

Paginate

The Paginate brick can be used immediately following an HTTP brick to fetch more results in the case that results are paginated. It functions like a while loop, running the defined HTTP call to fetch new results as long as there is a valid value at the path location.

Template:

{
	"brick": "paginate",
	"config": {
		"path": "",
		"call": {
			"brick": "http",
			"config": {
				"method": "",
				"auth": true,
				"url": "",
				"headers": {},
				"query": {},
				"body": {}
			}
		}
	}
}

Fields:

  • path, the path (relative to prevData) where the information necessary to get the next page of results is stored. While there is a valid value at this path, the Paginate brick will continue to run the pagination loop. Do not use Mustache to indicate path.
  • call, an object that contains the brick you would like to call, usually an HTTP Brick. This brick operates as normal, and you can use Mustache to hash in the relevant pagination data into your HTTP request. Inside this brick, prevData refers to the output of the previous HTTP request (see example below for more details).

Example:

{
	"brick": "paginate",
	"config": {
		"path": "nextPageToken",
		"call": {
			"brick": "http",
			"config": 
			"method": "GET",
			"auth": true,
			"url": "http://api.fakeapp.com/rest/messages/list",
			"query": 
				"nextPageToken": "{prevData.nextPageToken}
			}
		}
	}
}	

In this example, the API returns an object that contains metadata about pagination along with the list of results. The Paginate brick checks to see that there is a valid value at the path nextPageToken in the results from the previous brick. Then, it runs the defined HTTP call. Note how in the query, the value {{prevData.nextPageToken}} is explicitly referenced using Mustache. Each loop of the Paginate brick can refer the output of the previous call as prevData. The first time the Paginate brick is run, prevData is equivalent to the data of the previous brick. However, the second time it is run, prevData refers to the data output by the HTTP call that the Paginate brick just made.

Parse

Parse takes a string of javascript-escaped plaintext and parses it into JSON. It's useful for APIs that return plaintext responses instead of JSON. This brick is equivalent to the JavaScript JSON.parse() method.

Template:

{
	"brick": "parse",
	"config": {
		"JSONString": ""
	}
}

Fields:

  • JSONString, the string you would like to parse. You can use mustache templates to reference a piece of data from a previous brick.

Example:

{
	"brick": "parse",
	"config": {
		"JSONString": "{{prevData}}"
	}
}

This example will take data that looks like this:

{\r\n \"Fellowship\": {\r\n \"Gandalf\": {\r\n \"class\": \"wizard\",\r\n \"role\": \"guide\"\r\n },\r\n \"Frodo\": {\r\n \"class\": \"hobbit\",\r\n \"role\": \"ringbearer\"\r\n },\r\n \"Sam\" :{\r\n \"class\": \"hobbit\",\r\n \"role\": \"gardner\"\r\n },\r\n \"Merry\": {\r\n \"class\": \"hobbit\",\r\n \"role\": \"comic relief\"\r\n },\r\n \"Pippin\": {\r\n \"class\": \"hobbit\",\r\n \"role\": \"assistant comic relief\"\r\n },\r\n \"Aragorn\": {\r\n \"class\": \"human\",\r\n \"role\": \"sword\"\r\n },\r\n \"Legoalas\": {\r\n \"class\": \"elf\",\r\n \"role\": \"bow\"\r\n },\r\n \"Gimli\": {\r\n \"class\": \"dwarf\",\r\n \"role\": \"axe\"\r\n },\r\n \"Boromir\": {\r\n \"class\": \"human\",\r\n \"role\": \"face-heel turn\"\r\n }\r\n }\r\n}

And transform it into valid JSON:

{
  "Fellowship": {
		"Gandalf": {
		  "class": "wizard",
		  "role": "guide"
		},
		"Frodo": 
		  "class": "hobbit",
		  "role": "ringbearer"
		},
		"Sam" :
		  "class": "hobbit",
		  "role": "gardner"
		},
		"Merry": 
		  "class": "hobbit",
		  "role": "comic relief"
		},
		"Pippin": 
		  "class": "hobbit",
		  "role": "assistant comic relief"
		},
		"Aragorn": 
		  "class": "human",
		  "role": "sword"
		},
		"Legoalas": 
		  "class": "elf",
		  "role": "bow"
		},
		"Gimli": 
		  "class": "dwarf",
		  "role": "axe"
		},
		"Boromir": 
		  "class": "human",
		  "role": "face-heel turn"
		}
  }
}

Render

Renders an object using mustache templates.

Template:

{
	"brick": "render",
	"config": {
		"output": { 
			"": {
				"": ""
			}
		}
	}
}

Fields:

  • output, an object containing the data you would like to render

Example:

{
	"brick": "render",
	"config": {
		"output": {
			"Comment": {
				"Text": "{{allData.1.data.text}}",
				"Time Created" : "{{allData.1.data.timestamp}}"
			},
			"Author": {
				"Full Name": "{prevData.first_name} {prevData.last_name}",
				"Username": "{prevData.username}",
				"Email": "{prevData.email}"
			}
		}
  }
} 

The above example would produce an object that looks like this, given that the mustache is correct:

{
	"Comment": {
		"Text": "Nothing but the rain.",
		"Time Created": "2015-11-04T19:05:18+00:00",
	},
	"Author": 
		"Full Name": "Kara Thrace",
		"Username": "Starbuck462753",
		"Email": "kthrace@galactica.gov"
	}
}

Scope

Grabs a single field of the incoming data. This field can be a value, an object, or an array.

Template:

{
  "brick": "scope",
  "config": {
		"path": ""
  }
}

Fields:

  • path, the path of the field you would like to isolate

Example:

{
	"brick": 'scope',
	"config": {
		"path": "results.0"
	}
}

Since

The Since brick saves a value between FLO executions. You can then use the mustache variable {{{since}}} to reference this value the next time the FLO runs. This allows you to only fetch new data from the API, by using the {{{since}}} value from the previous execution to query the API for new results. Often, the value saved by the Since brick is a timestamp, but it can also be a record ID if the external API supports fetching records that have been created/updated since a certain ID.

A Since brick should be present in every Event method (except in the case of a webhook-driven event). The Since brick is not used in Metadata or Action methods.

Template:

{ 
	"brick": "since",
	"config": {
		"path": "",
		"format": "",
		"invalid": []
	}
}

Fields:

  • path, the path to the value you would like to save. If "path" is left empty, the engine will save the current time when the FLO is run
  • format, if path is left empty and the current time is saved instead, the format the current time should be saved in (options: "unix", "iso8601", "iso8601+0000", "yyyy-MM-dd HH:mm", "yyyy-MM-dd HH:mm:ss")
  • invalid, an optional array containing since values that would be considered invalid, separated by commas

Example:

In the HTTP brick, the since value from the previous time this Event was run is used to run a call and fetch an array of new records. If this is the first time the request has been run, this value will be left empty.

{
	"brick": "http",
	"config": {
		"method": "GET",
		"url": "https://api.github.com/repos/{{params.org}}/{{params.repo}}/issues",
		"headers": {
			"Authorization": "token {{auth.access_token}}"
		},
		"query": {
			"since": "{{since}}",
			"sort": "created"
		}
	}
}

Then, the since value is updated to the current time in iso8601 format.

{
	"brick": "since",
	"config": {
		"path": "",
		"format": "iso8601",
		"invalid": []
	}
}

Sort

The Sort brick sorts an array of objects in ascending order by the value at a specified key.

Template:

{
	"brick": "sort",
	"config": {
		"path": ""
	}
}

Fields:

  • path, the path of the value you would like to sort by.

Wrap

The Wrap brick wraps the incoming data in an object.

Template:

{
	"brick": 'wrap',
	"config": { 
		"in": ""
	}
}

Fields:

  • in, the object you would like to wrap prevData in

Example:

{
	"brick": 'wrap',
	"config": {
		"in": "results"
	}
}

Release Notes

12/15/2015

New Features:

  • Documentation is now located inside of the Forge Interface
  • New "Date" Brick(#date) for getting the current date and converting ISO8601 to Unix
  • Robust logging, logs the result of each brick in live-time rather than waiting for the finished output
  • You can clear the console by clicking the <i class="fa fa-ban"></i> icon in the right corner
  • You can now run OAuth from inside the Authentication section (instead of having to open a method) and see robust logging about the OAuth exchange

12/8/2015

New Features:

  • Test run data is saved between runs so you do not have to re-enter data every time you test a method

Bug Fixes:

  • Users now get an error message if they attempt to copy a method and save it without changing name
  • Font loading bug fixed
  • Security bug that listed details such as access token or API key in error tickets from the API fixed
  • Bug where forge users could not update "realtime available" field fixed

Known Issues:

  • Auth run data for basic and custom is not saved between runs
  • Method duplicate names error only runs for copied methods, not new methods

12/1/2015

New Features:

  • You can now copy methods. You must rename any copied methods, if two methods have the same name it will break your Channel.
  • You can now copy channels, but you must save your Channel under a new draftname.
  • You can now rename methods. This will break any FLOs running on this method, so only rename a method if you are confident that there are no FLOs that use it.
  • New brick: Branch(#branch) lets you make conditional statements and branch your method, calling metadata methods if conditionals are true.

11/20/2015

New Features:

  • The Channels dropdown has been alphebatized, and you can now type into a search box to find the channel you want
  • Favicon was updated to make it easier to tell the Forge and Designer tabs apart.
  • New brick: Call(#call) lets you call metadata methods from inside the brick steps.

11/17/2015

New Features:

  • Console now "squishes" editor instead of opening over it. You can also control the size of the windows by dragging.
  • Popup notifications for saving Channels, breaking JSON errors, and Channel submission now appear in the left corner and do not need to be manually closed.
  • Developers receive warnings if they try to navigate away from an unsaved Channel either by closing the window or switching to a new Channel.
  • When users are first setting up authentication, they can select a template (rather than copy/pasting).
  • You can now use hotkeys (e.g. ctrl-s or cmd-s) to save your work.
  • New error handling capability added to the HTTP brick, see the documentation(#http).

Bug Fixes:

  • Fixed saving bug, saving should not revert your workspace to the last saved version anymore.
  • Fixed bug where OAuth credentials were saved inside the session even when you switched Channels. You can now switch between 2 channels and it will re-run your authentication to get new credentials, although you will have to refresh if you update your authorization inside a Channel and want to re-run.

11/2/2015

New Features:

  • You can delete bricks and methods
  • You can add a brick above or below another brick
  • You can see the indices of the bricks
  • If you are a member of multiple organizations, you can switch between those organizations in a dropdown in the top-right corner and see the Channels scoped to each organization in your drafts drop-down
  • When you import code, you can choose to import as an existing Channel or as a new Channel

Bug Fixes:

  • Previously, saving your Channel has had a dependency on the "name" field in the General section, resulting in a disconnect between assigned drafts/saved drafts. This dependency has been fixed, but will require a merge of the branched drafts and some cleanup of junk drafts
  • Fixed problem where you can't type into the Auth window without the cursor jumping to the end of the line after every character
  • Console output has been cleaned up (no more unescaped HTML)
  • Adding a new brick takes you to that step
  • Saving doesn't take you back to the "General" workspace
  • Bug where you couldn't close the console if the editor window ran down the screen has been fixed

Other notes:

  • Right now, there is a problem scoping internal Channels to the contractor orgs. Currently being addressed, hotfix will be deployed when a solution is found.
  • Previous bugs in the saving UX resulted in the proliferation of "junk" drafts--drafts of Channels that are not saved to the right module name in the back end (e.g. the googlesheets Channel was saved as draft "Google Sheets" because of the dependency taken on the display name). When the scoping bug is fixed, we will send contractors instructions on how to merge their drafts into the draft with the right module name.

10/26/2015

New Features:

  • You can now switch between organizations inside of Forge, to ensure that you are seeing the correct Channels scoped to your account.
  • New look for the UI--lighter color scheme designed by Yuhan to make the transition from the designer to the channel builder less abrupt
  • You can now submit new versions of a Channel from inside betaforge by incrementing the version in the General section, then hitting the "Submit" button. You can expose this version of the Channel in beta by going to betaadmin, going to the Channel upload tool, and selecting "Load via S3." Then, submit the version the way you normally would from production.

Other notes:

  • To be able to work on, save, and submit a channel, it must be scoped to the org you are in. If you try to save a draft with the name of an existing channel, the save will not work.
  • Known bug with the dependency between the name of the channel and the draft name--the displayname of the Channel is currently being automatically lowercased to turn it into the draft name.

10/16/2015

New Features:

  • The text editor is out, the JSON editor is in! We've added a fully-featured code editor to forge, with syntax highlighting, folding/unfolding, bracket completion, and error notices
  • Forge is now in beta at betaforge.azuqua.com. You will now need a beta environment account to log in.
  • The channels you create while in Forge will be scoped to your organization and saved between sessions. Access your Channels from the dropdown menu in the secondary nav bar.
  • You no longer have to save your workspace before moving to another step. As long as what you've typed is valid JSON, your workspace will save for you as a type.
  • You do have to save your workspace before exiting your session or moving to another Channel project. Do this by hitting the save button. You will get a message telling you your code has saved--if you don't get that message, it hasn't saved

Other notes/known bugs:

  • Sometimes, after saving your project, the current workspace will revert to the template. Your work is not lost--simply navigate away and back again to see your work.
  • Currently, you cannot build or edit basic authorization within the channel builder. You may import a channel that has basic authorization, but you cannot edit the auth section. One workaround is to change the auth type to "custom." This will not effect the rest of your channel unless you use the "auth" field in your HTTP brick to build the auth header. If you do, you will have to explicitly add the header "Authorization":"Basic {auth.username}:{auth.password}" (or whatever matches your authparams).
  • Deleting methods/zebricks is still not possible, but will be coming soon. To delete a zebrick, export your channel and paste it into a text editor, delete the code manually, and import it back in. Then hit save.
  • You cannot currently change the name of a Channel without creating errors. This is also important if you decide to export/reimport--do not change the name of the Channel before re-importing.

10/2/2015

New Features:

  • Users can edit and save Channel JSON within the Channel Designer. This is useful for fixing bugs, since it allows developers to make a small change to a Channel and immediately run it to see if their change is fixed.
  • Users can build a new Channel from scratch using pre-built templates for inputs, outputs, parameters, and Zebricks.
  • Remote access is working, meaning users can run Channels without having the full stack on their computer.
  • OAuth verification is working, meaning that users can set up OAuth 1 or 2 then test it right in the Channel Builder, and use the token from the exchange to run their Channels.

Other notes:

  • You must manually save your work before navigating to another step by hitting the save button in the corner. Auto-save is not yet enabled.
  • Azuqua does not store your work beyond your session. If your Channel is unfinished at the end of your session, you must export your Channel, copy the code into a file, and then import it when you are ready to start work again.
  • Due to a change in positioning terms, Events have been mis-named "Triggers" in the Channel Builder. To add a new Event, go to the "Triggers" section.
  • After you add a new brick to your method, you must navigate away and return to your workspace to see the method displayed.
  • Currently, to build a Channel inside the Channel Builder, you must write your JSON in a plain text window. In future releases, you will be able to write and edit JSON using syntax highlighting, automatic bracket closing, and other developer-friendly features.
  • You cannot currently edit the names of your Events, Actions, or Metadata methods from within the Channel Builder. You can edit these names by exporting your Channel and entering an alternate value in the localization file at the mapping location for your method name.
@jacksonsouza
Copy link

9/15
Working on Salesforce Issue (BLOCKED)

  • "https://" must be explicitly typed in before "alphaforge.azuqua.com" in the browser bar, otherwise I get redirected to azuqua.com
  • The issue of not having close buttons on any dialog windows is bothersome. I have to refresh the browser & re-import my data every time I'd like to escape the dialog b/c the channel I am working on does not persist. I could press submit to close the dialog, however sometimes (particularly with invalid JSON) submit will not actually close the dialog.
  • Does imported channel data not persist? Do I have to re-import every time I refresh?
  • Forge keeps crashing when I try to run the channel with the Authentication method in the workspace. This turned out to be a result of erroneous custom code (Salesforce has a lot of required modules, which I had to stuff into one file).
  • There is no blocking or indication to the user that custom code is erroneous.
  • In order to switch from tab to tab and not lose data, you must hit the Save button. Page refresh wipes all data.

9/16
Working on Salesforce Issue (BLOCKED)

  • Was able to obtain access token from Osmond. These last for one hour, and I am able to obtain them through an unsafe, but well hidden way in the Chrome Developer Console. I should be able to obtain access through oAuth immediately in Forge.
  • Forge crashes on monitor method run with no parameters (env, instance_url, object). If the monitor is not run with parameters, it should spit back INVALID CONFIG, not crash.
  • The appropriate way to run a monitor with env, instance_url, object (in the case of Salesforce & others, as I'm sure it differs from method to method and channel to channel) should be documented - this is not a straightforward task but some guidance/connections between the RUN textareas and the GUI card need to be made for devs.
  • I cannot edit the top textarea on Run with env and instance_url. I tried to edit the raw code object in the Developer Console but was unable to.

@jacksonsouza
Copy link

9/29
Began work on Salesforce "Get User" Action
[3hrs, received new assignment then switched from Salesforce to Twitter b/c of issues]

  • oAuth2 is not supported with Azuqua consumer information, user must get own secret, key and callback. UPDATE: oAuth2 is still non-functional, callback routes pertaining to the service need to be updated.
  • Pasting into authorize method (maybe any text area) screws up parsing on run, assuming the above scenario, user must enter key, etc into the channel json before Import.
  • Twitter channel (oAuth1) sporadically works.

9/30
Began work on new Zendesk Monitor
[4hrs, switched from Twitter to Zendesk b/c of issues (oAuth1 & 2 are verified broken), spent time attempting to get a valid response for Zendesk on one action "Create".]

  • Zendesk channel in designer does not allow user to submit a new account configuration.
  • We do not have credentials for Zendesk, created my own, learned essential features.
  • Submit button in Forge does not, Run does not save repetative information such as username and password.
  • If Run panel does not Submit, user will need to check the logs, every textarea expects valid json.
  • If the user needs to edit any data in the Run panel, the text areas are way too small.
  • To construct new channels or monitors or methods, user is forced to develop in an external editor, import and then test for response (over and over).
  • It is difficult to discern how a valid input object is constructed, I need to refer to custom code, responses, Zendesk API and the Designer to get one valid response. (This may be normal). UPDATE: A json object in the channel formatted for the designer may not be a correct format for forge (more on this later).

@jacksonsouza
Copy link

Post-Zendesk 10/8

Chelsea, great job on the docs and managing the forge team. I can see an insane amount of progress being made, good work! Here are some notes on three things: your Docs, Forge itself & traversal of the Forge - Designer gap. On the last point, there is a considerable no-mans-land between the completion of a channel in Forge & the completion of a channel in the Designer. Many of these points are geared towards closing that gap.

Docs


  • I was confused by the distinction between Inputs and Params. I think it would be better to really "drive home" that Inputs are fields that receive input from other cards' output in the flo, while Params are user-edited text-fields, dropdowns or .. also used as input by the flo.

  • It needs to be abundantly clear (highlighted, underlined & bolded!) that the output of the Zebricks must match the Outputs, especially in the case of an Output metadata call, which is less easily constructed than an explicitly defined output object. This scenario goes along with the next point.

  • There needs to be more documentation on metadata calls, esp. Output metadata. For instance

    // OUTPUT OF ZEBRICKS (for a monitor)
    // [
    // {
    // "group_id": null,
    // "notes": "",
    // "details": "",
    // "name": "Testorito",
    // "updated_at": "2015-10-02T18:46:21Z",
    // "enterprise_id": 55555,
    // "yes_or_no": "no"
    // },
    // {
    // "group_id": null,
    // "notes": null,
    // "details": null,
    // "name": "Azuqua",
    // "updated_at": "2015-09-30T19:53:27Z",
    // "enterprise_id": null,
    // "yes_or_no": null
    // }
    // ]

    // RETURNED BY OUTPUT METADATA CALL
    // {
    // "OutputFields": [
    // "group_id",
    // "notes",
    // "details",
    // "name",
    // "updated_at",
    // "yes_or_no",
    // "enterprise_id"
    // ]
    // }

is acceptable. This is dramatically different than assembling the output in the channel json. Metadata calls are essential to building complex monitors, I'm sure I'll have more input on this as I move along.

  • There needs to be more documentation on custom code: here a are a few things I learned.

options.params - gives params obj
options.prevData - gives data of previous brick
options.input - gives input obj
options.config - gets config passed to custom code in channel json

Forge


  • In the run panel, the Since field is marked with a placeholder, which is very helpful. The other fields should be marked as well.
  • Input & output metadata calls should work directly from the parent monitor/action's run panel. Lito mentioned that this has not been implemented, is this a possibility?
  • How will we test/write tests for channel methods?

Gap


  • How will forge-based in-house channel developers (like myself) be able to test their channel's interaction with the designer? Surely there is/will be an easier way than what is/will be available for external channel developers. The additional walls of security & screening that we enforce for external forge devs should be relaxed & made streamlined for internal devs. I'd like a "Push to Alpha" button, but that may or may not be prudent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment