# Connector building - Building actions

Now that we’ve defined objects schema in methods, we can now start building out our CRUDS actions which will reference the schema methods we have just defined.

# Defining config fields

When dealing with object-based actions, we first need to define something called configuration fields. Configuration fields are special input fields that you can define whose answers can dynamically generate other input fields.

config_fields: [
  {
    name: 'object',
    optional: false,
    label: 'Object type',
    control_type: 'select',
    pick_list: 'object_list_create',
    hint: 'Select the object type from picklist.'
  }
],

Config fields Selecting invoice causes invoice related input fields to appear

Here we also introduce a picklist which we can easily add additional objects as we introduce support for them.

Configuration fields can also be used in various ways to dynamically generate input fields based on what the user has selected. In cases where an object selected still requires more information before input fields can be accurately shown to the user, we go through some patterns later in this guide that can do so.

# Defining your title, subtitle, description, and help text

It is also highly recommended and really important to define helpful titles and descriptions for your actions. When dealing with object-based actions, this helps with the readability of recipes using your connector as well as improves user experience for those building recipes with your connector.

actions: {
  create_object: {
    title: 'Create object',

    subtitle: 'Supports the creation of invoices, payments, and customers',

    description: lambda do |input, picklist_label|
      "Create a <span class='provider'>" \
      "#{picklist_label['object'] || 'object'}</span> in " \
      "<span class='provider'>XYZ Accounting</span>"
    end,

    help: lambda do |input, picklist_label|
      "Creates an #{picklist_label['object'] || 'object'} in XYZ. First, select from a list of " \
      'objects that we currently support. After selecting your object,' \
      ' dynamic input fields specific to the object selected ' \
      'will be populated.'   
    end,

    config_fields: [
      {
        name: 'object',
        optional: false,
        label: 'Object type',
        control_type: 'select',
        pick_list: 'object_list_create',
        hint: 'Select the object type from picklist.'
      }
    ],
    # More code truncated here
  }
}

Over here we define title and subtitles to give users an idea of the action out of all the different actions in your connector. Remember to keep your title concise whilst using subtitles to provide a bit more information.

For descriptions, we allow you to use a lambda function (as shown in the example above) to dynamically change the description of the action when a user makes a selection in the config_field. The same can be done for help text as shown in the example above.

Bad example of dynamic descriptions Bad example with no dynamic description

Good example of dynamic descriptions Good example with dynamic description

# Defining input fields

Another feature of creating clean and scalable input fields are that a single action can support multiple objects with a single object_definitions call. Since the value of configuration fields can only be accessed through an object_definitions method, we suggest calling a generic object_definitions that can later pull the appropriate schema based on the object the user has selected.

# Input fields

input_fields: lambda do |object_definitions, connection, config_fields|

  object = config_fields['object']

  input_schema = object_definitions[object] # If user select invoice, evaluates to object_definitions['invoice']
  
  case object
  when 'invoice'
    input_schema.where('name !=': 'Id')
  else
    input_schema
  end
end,

# Object definition

  object_definitions: {
    invoice: lambda do |connection, config_fields, object_definitions|
      [
        { name: "Id" },
        { name: "TxnDate" },
        { name: "TotalAmt", type: "number" },
        {
          name: "Line",
          type: "array",
          of: "object",
          properties: [
            { name: "Description" },
            {
              name: "SalesItemLineDetail",
              type: "object",
              properties: [
                { name: "Qty", type: "number" },
                { name: "UnitPrice", type: "number" }
              ]
            },
            { name: "Line-Num", type: "number" },
            { name: "Amount", type: "number" },
            { name: "Id" }
          ]
        },
        # More schema
      ]
    end
  }

When the object definition invoice is called, the schema relevant to the invoice is returned. In the input_fields, we then conditionally filter out the id field as it is not applicable to the creation of an invoice and should not be shown.

# Defining the execute block

When defining the execute block, we first store any generic methods for pre processing or post processing of data in the execute block. In the example below, use to generic methods that format payloads before being sent out the target API. After the formatting of the payload, a call is then made to a method which houses any specific data processing that needs to be done on a action-object level as well as the final HTTP call to the appropriate endpoint.

Another key feature to take note is the use of error handling to surface appropriate error messages for users of your connectors. During recipe design time and the debugging of any errors, these error messages save large amounts of time for users

# Execute block

execute: lambda do |connection,input|
  object_name = input.delete('object')
  
  response = call('create_#{object_name}_execute', payload).
              after_error_response(/.*/) do |_code, body, _header, message|
                error("#{message}: #{body}")
              end
  
  formatted_response
end,

# action-object method

create_invoice_execute: lambda do |payload|
  post('api/invoice/create', payload)
end,

# Defining the output fields

Output fields can be defined in the same way as input fields using the same schema method used earlier. When calling the schema method, remember to pass the parameter output so your method knows to return fields expected in the response. Often this includes metadata about the object that cannot be changed by users such as created_at or updated_at timestamps.

# Output fields

output_fields: lambda do |object_definitions, connection, config_fields|
  object = config_fields['object']

  input_schema = object_definitions[object] # If user select invoice, evaluates to object_definitions['invoice']
end,

No further manipulation is needed as the invoice schema contained in object_definition['invoice'] matches the fields returned from the API exactly.

# Example 1: Update invoice action in XYZ Accounting

Below, we go through one full example for an update object action in XYZ accounting.

# Sample code

methods: {
  update_invoice_execute: lambda do |payload|
    patch('api/invoice/update', payload)
  end,

},

object_definitions: {

  invoice: {
    fields: lambda do |connection, config_fields, object_definitions|
      # same schema as above
    end
  },

},
actions: {

  update_object: {
    title: 'Update object',

    subtitle: 'Updates an object in XYZ accounting.',

    description: lambda do |input, picklist_label|
      "Update a <span class='provider'>" \
      "#{picklist_label['object'] || 'object'}</span> in " \
      "<span class='provider'>XYZ Accounting</span>"
    end,

    help: lambda do |input, picklist_label|
      {
        body:
        "Updates an #{picklist_label['object'] || 'object'} in XYZ. First, select from a list of " \
        'objects that we currently support. After selecting your object,' \
        ' dynamic input fields specific to the object selected ' \
        'will be populated.'   
      }
    end,

    config_fields: [
      {
        name: 'object',
        optional: false,
        label: 'Object type',
        control_type: 'select',
        pick_list: 'object_list_update',
        hint: 'Select the object type from picklist.'
      }
    ],

    input_fields: lambda do |object_definitions, connection, config_fields|
      object = config_fields['object']

      input_schema = object_definitions[object]
    end,

    execute: lambda do |connection,input|
      object_name = input.delete('object')

      response = call('update_#{object_name}_execute', payload).
                  after_error_response(/.*/) do |_code, body, _header, message|
                    error("#{message}: #{body}")
                  end

      response
    end,

    output_fields: lambda do |object_definitions, connection, config_fields|
      object = config_fields['object']

      input_schema = object_definitions[object]
    end,

    sample_output: lambda do |connection, input|
      payload = {
        "limit" => 1,
        "status" => "closed"
      }
      call("search_#{input['object']}_execute", payload)
    end

  }
  # More actions below
},

pick_lists: {
  object_list_update: lambda do
    [
      ["Invoice","invoice"]
    ]
  end,
  # More picklists below
}

The example below showcases all the different steps needed to create an update object action. Currently this code only shows support for a single object - Invoices. The structure for the update object action is largely identical to that of the create object action where configuration fields, titles, subtitles, descriptions, help text, input fields, output fields, execute, and sample output blocks are defined generically. Most of the magic happens in object definitions and methods where object-specific code is introduced to retrieve specific schema and make HTTP calls to endpoints related to that object.

# Building triggers

Lets move on to learning about building object based triggers.


Last updated: 3/29/2023, 2:00:59 PM