{"id":13800150,"url":"https://github.com/workato/custom_connector_docs","last_synced_at":"2025-05-13T09:31:08.533Z","repository":{"id":33873908,"uuid":"143073259","full_name":"workato/custom_connector_docs","owner":"workato","description":null,"archived":false,"fork":false,"pushed_at":"2022-01-11T18:07:59.000Z","size":2161,"stargazers_count":43,"open_issues_count":43,"forks_count":53,"subscribers_count":59,"default_branch":"master","last_synced_at":"2024-11-18T15:01:55.566Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/workato.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-07-31T21:44:08.000Z","updated_at":"2024-07-13T19:40:39.000Z","dependencies_parsed_at":"2022-07-28T21:08:55.026Z","dependency_job_id":null,"html_url":"https://github.com/workato/custom_connector_docs","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/workato%2Fcustom_connector_docs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/workato%2Fcustom_connector_docs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/workato%2Fcustom_connector_docs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/workato%2Fcustom_connector_docs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/workato","download_url":"https://codeload.github.com/workato/custom_connector_docs/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253913027,"owners_count":21983244,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-04T00:01:09.853Z","updated_at":"2025-05-13T09:31:07.172Z","avatar_url":"https://github.com/workato.png","language":"Ruby","funding_links":[],"categories":["Reference Resources"],"sub_categories":["Workato"],"readme":"[![Workato](https://github.com/workato/custom_connector_docs/raw/gh-pages/images/workato_logo.png)](https://www.workato.com)\n\n# Introduction\n\nWelcome to the Workato Developer Program website. Here you will find the documentation you need to build application adapters using our SDK.\n\nAlternatively, Workato offers a [HTTP Connector](http://bit.ly/WorkatoHTTPConnector) where you can create your own app connectors without having to code.\n\n## Recipe\n\nA Workato recipe is a set of predefined instructions to be executed. It is made up of a trigger and one or more actions.\n\nIt execute a variety of tasks to all the applications supported by the Workato platform.\n\n## Trigger\n\nDefines an event that triggers the execution of a Workato recipe\n\n## Action\n\nSteps that will be executed when a recipe is triggered.\n\nThere are 4 types of actions:\n\n1. **Action**\n  - Basic actions perform tasks like Search, Create, Update\n2. **Conditional action**\n  - These actions behave like traffic control. They provide users with the capability to restrict certain actions based on conditions.\n  - Example: Create a new Account only if it does not already exist\n3. **Repeat action**\n  - Repeat actions are simple loops. They perform predefined tasks multiple times based on an array (list) of records.\n  - Example: Add line items in QuickBooks for each opportunity item in Salesforce\n4. **Stop**\n  - Allows users to terminate a run of the recipe (a job). This is useful if you wish to stop performing any actions if a certain condition is met.\n  - Optionally, you can define an error for this action. What this does is let you generate exceptions in certain scenarios. These jobs that stops with errors will show up in job history as errors\n\n## Adapter\n\nAn Adapter is a connector to an application. Each Adapter has one or more trigger and actions. When an action or trigger is executed, it performs it's respective function with the account instance connected to that application.\n\n# Connector SDK\n\n## Custom Adapters\n\nConnectors built on the SDK are called **custom adapters**. These connectors have private scope. This means that they are only available to the connector owner.\n\nTo enable global scope for this connector, the code will need to go through review by the Workato team. You can begin this process by submitting a pull request to our [repository](https://github.com/workato/custom_connector_docs).\n\n## Requirements\n\n### Format\n\nPresently, we support the following types:\n  - JSON\n  - XML\n\n### Bonus\n\n#### Pagination\n\nPagination helps with response data that is more manageable. It is definitely a bonus if the intended API supports that.\n\n#### Query\n\nIt is very useful to be able to query resources instead of locating them based on IDs. With Search by query, the API allows you to return a list of results that matches your field criterias. You may also want to look out for the ability to query based on created/updated time which will be crucial when building out your triggers.\n\n## Authentication\n\n### Basic Authentication\n\n#### Standard\n\nTypically, a basic authentication requires a username and password combination when making requests. Make sure to include those two fields in the connection fields definition.\n\n```ruby\nconnection: {\n\n  fields: [\n    {\n      name: \"username\",\n      hint: \"Your email used for login\"\n    },\n    {\n      name: \"password\",\n      optional: false,\n      control_type: \"password\",\n    }\n  ],\n\n  authorization: {\n    type: \"basic_auth\",\n\n    credentials: lambda do |connection|\n      user(connection[\"username\"])\n      password(connection[\"password\"])\n    end\n  }\n}\n```\n\nTo set up a basic authentication, simply define type: 'basic_auth' and include the appropriate values in `user()` and `password()` in the credentials section. All requests made through the connector will contain the values defined in credentials.\n\n#### Variations\n\nSome APIs expect different conventions from a standard basic authentication.\n\n```ruby\nconnection: {\n  fields: [\n    {\n      name: \"api_key\",\n      optional: false,\n      hint: \"Profile (top right) \u003e Settings \u003e Your API Keys\"\n    }\n  ],\n\n  authorization: {\n    type: \"basic_auth\",\n\n    # close.io uses api key only for authentication. treats apikey as username and password left blank\n    # curl -u \"{your api_key}:\" \"https://app.close.io/api/v1/me/\"\n    credentials: lambda do |connection|\n      user(connection[\"api_key\"])\n      password(\"\")\n    end\n  }\n}\n```\n\nIn this example, Close.io API expects an API Key generated in the individual User’s account. It should be used as a username with a blank password in the standard basic authentication format.\n\nSo, to adjust the connections portion of the code to suit this behaviour, simply request for an API instead of username + password.\n\nIn the credentials section, pass the api_key into `user()` and an empty string (“”) to `password()`\n\n```ruby\nconnection: {\n\n  fields: [\n    {\n      name: \"api_token\",\n      control_type: \"password\",\n      label: \"API token\",\n      optional: false,\n      hint: \"Available in 'My Profile' page\"\n    }\n  ],\n\n  authorization: {\n    type: \"basic_auth\",\n\n    # Toggl API expect the token to be sent as user name and the string 'api_token' as the password\n    # curl -u \"{your api_token}:api_token\" \"https://www.toggl.com/api/v8/me\"\n    credentials: lambda do |connection|\n      user(connection[\"api_token\"])\n      password(\"api_token\")\n    end\n  }\n}\n```\n\nAnother variation is to have a generated api_token replace the user name and have the string “api_token” replacing password in the basic authentication format.\n\n### API Key Authentication\n\nFor APIs that expect API Key authentication, it is a slight variation from the basic authentication code above.\n\nMake sure to include the required inputs from the user (subdomain, api_key, scope etc)\n\nDefine\n1. type: \"api_key\"\n2. the appropriate parameter name for the api_key. In this case, it is simply \"api_key\"\n\nAfter defining this, calls will have the appropriate params appended.\n\nExample:\n`\u003cBASE_URL\u003e/users?api_key=NB674921`\n\n```ruby\nconnection: {\n\n  fields: [\n    {\n      name: \"helpdesk\",\n      control_type: \"subdomain\",\n      url: \".freshdesk.com\",\n      optional: false,\n      hint: \"Your helpdesk name as found in your Freshdesk URL\"\n    },\n    {\n      name: \"api_key\",\n      control_type: \"password\",\n      optional: false,\n      label: \"API key\"\n    }\n  ],\n\n  authorization: {\n    type: \"api_key\",\n\n    credentials: lambda do |connection|\n      params(api_key: connection[\"api_key\"])\n    end\n  }\n}\n```\n\n### OAuth 2.0\n\nFor a more secure method of authentication, we recommend using [OAuth 2.0](https://tools.ietf.org/html/rfc6749). It is an open standard and is generally a more secure way for users to log into third party websites without exposing their credentials.\n\n```ruby\nconnection: {\n\n  authorization: {\n    type: \"oauth2\",\n\n    authorization_url: lambda do\n      \"https://www.pushbullet.com/authorize?response_type=code\"\n    end,\n\n    token_url: lambda do\n      \"https://api.pushbullet.com/oauth2/token\"\n    end,\n\n    client_id: \"YOUR_PUSHBULLET_CLIENT_ID\",\n\n    client_secret: \"YOUR_PUSHBULLET_CLIENT_SECRET\",\n\n    credentials: lambda do |connection, access_token|\n      headers(\"Authorization\": \"Bearer #{access_token}\")\n    end\n  }\n}\n```\n\nThe Workato connector SDK currently supports the [Authorization Code Grant](https://tools.ietf.org/html/rfc6749#section-4.1) variant of the OAuth2 standard.\n\nRequired components in OAuth 2.0 type connection:\n\n1. type (use 'oauth2')\n2. authorization_url\n3. token_url\n4. client_id and client_secret\n5. credentials\n\nRedirect URI will be appended to the authorization request by the framework, so there is no need to include it. If the application requires that you register the redirect URI beforehand, use:\nhttps://www.workato.com/oauth/callback\n\nAdjust headers format as required in the credentials section\n\nFor example, Pushbullet expects the header to include token in this format:\n`OAuth2: \u003caccess token\u003e`\n\nSo to adjust to suit this requirement, define the credentials portion like so:\n\n```ruby\nconnection: {\n\n  authorization: {\n    type: \"oauth2\",\n\n    authorization_url: lambda do\n      \"https://podio.com/oauth/authorize\"\n    end,\n\n    token_url: lambda do\n      \"https://podio.com/oauth/token\"\n    end,\n\n    client_id: \"YOUR_PODIO_CLIENT_ID\",\n\n    client_secret: \"YOUR_PODIO_CLIENT_SECRET\",\n\n    credentials: lambda do |connection, access_token|\n      headers(\"Authorization\": \"OAuth2 #{access_token}\")\n    end\n  }\n}\n```\n\nNote:\n\n- SDK makes a POST request to token endpoint. Will not currently work for APIs expecting a different type of request.\n- Ensure that your implementation of OAuth 2.0 is compliant with the specifications stated in the RFC document. Else, your custom adapter might not start.\n  - For example, [Issuing an Access Token - Successful Response](https://tools.ietf.org/html/rfc6749#section-5.1) states that Workato will be expecting a response with the following required parameters: `access_token`, `token_type` and `expires_in`. Returning the access token with a key of `accessToken` in a JSON response will result in an unsuccessful Workato request to your `token_url`.\n  - Usually this will not be a problem because most OAuth libraries out there will do most of the heavily-lifting for you, such as returning response in the right format etc. It is good to be aware of this!\n\n### Custom Authentication\n\n\n\n## Action\n\n### Endpoints\n\nAn action can make one or more requests to various endpoints. Because the framework handles the authentication side of a request, you will not have to worry about that here.\n\nThe most important thing is to identify which endpoint will address the purpose of the action. Here we will take a look at Close.io's Lead object and how to retrieve it via the API.\n\n![close.io get lead object image](images/closeio-doc.png)\n\n```ruby\nactions: {\n\n  get_lead_by_id: {\n\n    input_fields: lambda do\n      [\n        { name: \"lead_id\", optional: false }\n      ]\n    end,\n\n    execute: lambda do |connection, input|\n      get(\"https://app.close.io/api/v1/lead/#{input[\"lead_id\"]}/\")\n    end,\n\n    output_fields: lambda do |object_definitions|\n      object_definitions[\"lead\"]\n    end\n  }\n}\n```\n\nA very simple action looks like this. A get request to the Close.io leads endpoint. In this case, the particular lead’s details is appended in the endpoint.\n\n### Parameter / Payload\n\nOther endpoints require parameters to access certain details, instead of accessing a particular resource route.\n\nA GET request can have parameters added to the request like so:\n\n```ruby\nexecute: lambda do |connection, input|\n  {\n    'companies': get(\"https://#{connection['deployment']}.api.accelo.com/api/v0/companies.json\").\n                 params(_search: input[\"company_name\"])[\"response\"]\n  }\nend\n```\n\nA POST or PUT or PATCH request can have payloads attached as well. There are 2 ways you can do this.\n\nAdd payloads to the method\n\n```ruby\nexecute: lambda do |connection, input|\n  {\n    \"users\": get(\"https://#{connection['helpdesk']}.freshdesk.com/api/users.json\", input)[\"results\"]\n  }\nend\n```\n\nAdd payloads using the payload method\n\n```ruby\nexecute: lambda do |connection, input|\n  post(\"https://api.pushbullet.com/v2/pushes\").\n    payload(\n      email: input[\"email\"],\n      title: input[\"title\"],\n      body: input[\"body\"]\n    )\nend\n```\n\n### Methods\n\nNot all ruby public instance methods are available. Methods are whitelisted to ensure security. The Workato SDK Framework also exposes some methods to make building SDKs convenient.\n\n\nHere is a list of methods available:\n\nREST verb methods\n- get(url, input)\n- post(url, input)\n- put(url, input)\n- patch(url, input)\n\nNote:\n\n- `input` is actually a Ruby Hash that will be converted to JSON by the Connector SDK. Also, since `input` is the last argument of the method, we can optionally omit the curly braces.\n\nMethod | Description\n--- | ----------\neach | Basic iterator\u003cbr\u003e`[1, 2, 3].each { |i| puts i }`\ngroup_by | [Group arrays into sets](http://apidock.com/rails/Enumerable/group_by)\nheaders | Add headers to a request\u003cbr\u003e`.headers(Authorization: \"Bearer HTB674HJK1\")`\nparams | Add parameter to a request\u003cbr\u003e`.params(api_key: \"HTB674HJK1\")`\npayload | Add payload to a request\u003cbr\u003e`.payload(id: \"345\")`\nignored | Ignore a comma-separate list of fields\u003cbr\u003e`object_definition[\"user\"].ignored(\"id\", \"created_at\")`\nonly | White list a comma-separate  of fields\u003cbr\u003e`object_definition[\"user\"].only(\"id\", \"name\")`\nrequired | Make a comma-separate list of fields required\u003cbr\u003e`object_definition[\"user\"].required(\"id\", \"created_at\")`\ninject | [Combine elements in an array using an operation](http://apidock.com/ruby/Enumerable/inject)\niso8601 | Convert a date/date-time variable to ISO8601 format\nmap | Returns a new array after invoking block on each element\nmerge | Returns a new hash containing [merged contents](https://ruby-doc.org/core-2.2.0/Hash.html#method-i-merge) of another hash\npluck | Select one or more attributes from an array of objects\u003cbr\u003e`[{\"id\": 1, \"name\": \"David\"},{\"id\": 2, \"name\": \"Peter\"}].pluck(\"id\")` returns `[1, 2]`\nrand | Random number between 0 and 1\nselect | [Selectively returns](http://apidock.com/ruby/v1_9_3_392/Array/select) elements for which the block returns true\nreject | [Selectively returns](http://apidock.com/ruby/v1_9_3_392/Array/reject) elements for which the block returns false (similar but opposite of select)\nsort | [Sort function](http://apidock.com/ruby/Array/sort) returning new sorted array\nsort_by | [Sort function](http://apidock.com/ruby/v1_9_3_392/Array/sort_by) returning self\nutc | Convert Time to [utc](http://ruby-doc.org/core-2.2.0/Time.html#method-c-utc)\nputs | ruby version of console.log/stdout, not the same as put\nwhile | [while loop statement](https://www.tutorialspoint.com/ruby/ruby_loops.htm)\n\n(This list can and will be expanded constantly, feel free to contact [me](eeshan@workato.com) to update/add to this list)\n\n## Poll Trigger\n\nRecords (tickets, leads, items etc.) are called events in a poll. A poll trigger constantly executes a poll block for new events at fixed time intervals. This time interval depends on a user's subscription (5 or 10 minutes). At the same time, it is also able to support pagination. This is useful to prevent request timeouts when making requests with large response results. A trigger can execute immediate consecutive polls to retrieve events from successive pages.\n\nThere are two types of trigger. The classic trigger type is used by default if `type` is not specified. The other type is called the \"paging_desc\" trigger, which can be used only for endpoints that provide events sorted in descending order. This trigger type has an auto-paging mechanism that lets you build a simple and efficient trigger. (described in detail below)\n\nA ruby hash is returned in each poll. This hash should contain a few keys. The array of events, or data, should be passed into the `events` key. At the same time, a cursor is saved in `next_page`/`next_poll` (depending on the trigger type). This cursor provides information about where the current poll stopped, and used in the next poll. A classic type trigger has an additional key `can_poll_more`, which can be defined to conditionally fire immediate polls for multi-page results.\n\n### Classic Trigger\nNo need to define any type to use the classic trigger. This type is usually used for APIs that only return responses sorted in ascending order.\n\n```ruby\nupdated_ticket: {\n  input_fields: lambda do\n    [\n      {\n        name: 'since',\n        type: :timestamp,\n        optional: false\n      }\n    ]\n  end,\n\n  poll: lambda do |connection, input, last_updated_since|\n    page_size = 100\n    updated_since = (last_updated_since || input['since']).to_time.utc.iso8601\n\n    tickets = get(\"https://#{connection['helpdesk']}.freshdesk.com/api/v2/tickets.json\").\n              params(order_by: 'updated_at',\n                     order_type: 'asc',\n                     per_page: page_size,\n                     updated_since: updated_since)\n\n    next_updated_since = tickets.last['updated_at'] unless tickets.blank?\n\n    {\n      events: tickets,\n      next_poll: next_updated_since,\n      can_poll_more: tickets.length \u003e= page_size\n    }\n  end,\n\n  dedup: lambda do |ticket|\n    ticket['id']\n  end,\n\n  output_fields: lambda do |object_definitions|\n    object_definitions['ticket']\n  end\n}\n```\n\n#### poll\n##### Arguments\nA poll block is given 3 arguments.\n\nThe first argument is `connection`, used to access inputs from connection field values. This is frequently used to access domain or subdomain information from the user.\n\n`input` provides access to trigger input field values from the recipe. Inputs like \"created_since\" a particular date is usually used in a trigger to allow filtering historic records. In this case, Knack does not provide filtering by record created dates, input is given an empty array.\n\nThe last argument, usually given the name `last_updated_since` or `last_created_since`, is the cursor \"stored\" from a previous poll. This is crucial to a good trigger design. It is used to determine where the last poll stopped and where to begin next. As an example, it is usually given the last (latest) \"updated\"/\"created\" time. When the trigger is first started, this value is `nil`.\n\n##### Output\n```ruby\npoll: lambda do |connection, input, last_updated_since|\n  page_size = 100\n  updated_since = (last_updated_since || input['since']).to_time.utc.iso8601\n\n  tickets = get(\"https://#{connection['helpdesk']}.freshdesk.com/api/v2/tickets.json\").\n            params(order_by: 'updated_at',\n                   order_type: 'asc',\n                   per_page: page_size,\n                   updated_since: updated_since)\n\n  next_updated_since = tickets.last['updated_at'] unless tickets.blank?\n\n  {\n    events: tickets,\n    next_poll: next_updated_since,\n    can_poll_more: tickets.length \u003e= page_size\n  }\nend\n```\n\nIn a classic type trigger, the expected output contains `events`, `next_poll` and `can_poll_more`.\n\n`events` expects an array of individual results to be processed through the recipe as individual events.\n\n`next_poll` is a cursor that will be passed on to the successive poll (third argument of `poll` block.\n\n**Important**:\nThis trigger type does not have automatic-immediate polling. Immediate polling is determined by `can_poll_more`, which is a boolean value for whether an immediate poll should be made.\n\nExample JSON response:\n```ruby\n[\n  {\n    \"id\": 1,\n    \"updated_at\": \"2016-08-13T00:53:44Z\"\n  },\n  {\n    \"id\": 2,\n    \"updated_at\": \"2016-09-14T02:25:00Z\"\n  },\n  ...\n]\n```\n\nWhen a get request receives this JSON response, it looks up the array for the last record (latest record) and passes it as the cursor for the next poll. It also checks the response array size. If it is equal to the size limit, it is likely that there are more records matching this request in consequtive pages. Hence the expression given to `can_poll_more` evaluates to true and invokes an immediate successive poll. This loop continues until response array size is smaller than page limit.\n\nAt the end of the loop. The last (latest) created date is passed as `next_poll`. This value will be used in the next poll cycle to pick up new records.\n\n#### dedup\n\nDedup block is used to identify individual events. It is given a single argument \"event\", which corresponds to individual elements in the records array passed into \"events\".\n\nA typical dedup input is `event[‘id’]` where the `event` argument name can be replaced to make the code more readable. This should be used only in classic triggers.\n```ruby\ndedup: lambda do |ticket|\n  ticket[\"id\"]\nend\n```\nIn some instances, a record needs to be proccessed as separate events. A typical scenario is updated records. To do this, append updated timestamp field to the dedup expression like so.\n```ruby\ndedup: lambda do |ticket|\n  ticket[\"id\"] + \"@\" + ticket[\"`updated_at\"]\nend\n```\nWith this, 2 occurence of a record with the same \"ID\" but with different \"updated_at\" values will be recorded as separate events.\n\n### Descending Trigger\n```ruby\nnew_alert: {\n  type: :paging_desc,\n\n  input_fields: lambda do\n    [\n      {\n        name: 'since',\n        type: :timestamp,\n        optional: false\n      }\n    ]\n  end,\n\n  poll: lambda do |connection, input, page|\n    limit = 100\n    page ||= 0\n    created_since = (input['since'] || Time.now).to_i\n    offset = (limit * page)\n\n    response = get(\"https://api.pingdom.com/api/2.0/actions\").\n                 params(from: created_since,\n                        limit: limit,\n                        offset: offset)\n\n    {\n      events: response,\n      next_page: (response.length \u003e= limit) ? page + 1 : nil\n    }\n  end,\n\n  document_id: lambda do |response|\n    response['checkid']\n  end,\n\n  sort_by: lambda do |response|\n    response['time']\n  end,\n\n  output_fields: lambda do |object_definitions|\n    object_definitions['alert']\n  end\n}\n```\n\n#### type\n`type: :paging_desc` - This type should be used only if results are in descending order. A `paging_desc` trigger works by assuming a descending order and continuously poll for all unique records. If the API is unable to return records in descending order, ignore this key to use the classic trigger.\n\nA record of all event IDs (defined in `document_id` ) is recorded for each recipe. Each recipe will \"remember\" all event IDs that is processed through a trigger.\n\nBased on assumption of order, the trigger can stop the polling cycle once a similar event IDs is observed. This is because further polls will return events \"before\" and would have already been processed by the trigger. At this point, the trigger stops polling and wait for the new poll cycle for new events. The Workato trigger framework handles deduplication in the background.\n\n#### poll\nPoll block is where you define how events are obtained. Typically, a GET request is used to retrieve a list of records to be processed as trigger events.\n\n##### Arguments\nDescending trigger `poll`s have the exact same argument structure. `connection`, `input` and `cursor`. The one difference is that the last argument is usually given the name `page`. In a descending trigger, this argument is usually used to pass the page number instead of record timestamp.\n\n##### Output\nSimilar to a classic trigger, the `events` in output here expects an array of results.\n\n```ruby\npoll: lambda do |connection, input, page|\n  limit = 100\n  page ||= 0\n  created_since = (input['since'] || Time.now).to_i\n  offset = (limit * page)\n\n  response = get(\"https://api.pingdom.com/api/2.0/actions\").\n               params(from: created_since,\n                      limit: limit,\n                      offset: offset)\n\n  {\n    events: response,\n    next_page: (response.length \u003e= limit) ? page + 1 : nil\n  }\nend\n```\n\nInstead of `next_poll`, `paging_desc` type trigger should be given `next_page` as cursor. It is used for passing the next page to be polled.\n\nA typical response looks like this.\n\n```ruby\n{\n  \"current\": \"https://api.sample.com/records?order=desc\u0026from=2016-12-09T22%3A57%3A13Z\u0026page=1\",\n  \"next\": \"https://api.sample.com/records?order=desc\u0026from=2016-12-09T22%3A57%3A13Z\u0026page=2\",\n  \"data: [\n    {\n      \"checkid\": 123,\n      \"time\": \"2016-12-13T19:09:01Z\",\n      ...\n    },\n    {\n      \"checkid\": 124,\n      \"time\": \"2016-12-10T06:20:00Z\",\n      ...\n    }\n  ]\n}\n```\n\n`response[\"data\"]` is an array of results from the request, which should be passed to `events`. While it varies between APIs, most will provide some form of pointer to the next page of results. In this example,`response[\"next\"]` is an API generated url to be used to retrieve the next page of results matching the request. Hence we can simply pass it to `next_page`. In the successive poll, this value is passed to the poll block and the next set of records is retrieved.\n\nOn the last page, `response[\"next\"]` is usually `null`. In that case, it will cause the successive poll to make the same request as in the first page. When this happens, the trigger framework detects a duplicate event (mechanism for detection explained in detail later) and halts all immediate polls. At this point, the trigger goes to \"sleep\" until the next poll cycle.\n\nNew records being added in the cloud instance will appear in the first page of requests since records are returned in descending order. The trigger picks up new records and processes them until a duplicate event is detected, at which point it \"sleeps\". This trigger type depends on a descending order to retrieve only \"new\" records and stops upon detection of \"old\" records.\n\nThis way, the trigger polls for new records efficiently, without making any repeated requests to retrieve the entire set of records in each poll cycle.\n\n### document_id\nSimilar to `dedup`, `paging_desc` type triggers uses `document_id` to identify unique records. Write an expression that can uniquely identify the \"ID\" of records.\n\n```ruby\ndocument_id: lambda do |event|\n  event[\"checkid\"]\nend\n```\n\nUnlike classic triggers, there is no need to include timestamp fields into the `document_id` expression to deduplicate updated records. Descending triggers have an additional block `sort_by` to process this.\n\n### sort_by\nIn addition to `document_id`, this block is used to define the field that is used to sort records (in descending order). It could be \"created_at\" or \"updated_at\" or similar fields.\n\n```ruby\nsort_by: lambda do |event|\n  eent[\"time\"]\nend\n```\n\n## Webhook Trigger\n\n```ruby\ntriggers: {\n  new_message: {\n    type: :paging_desc,\n\n    input_fields: lambda do |object_definitions|\n      object_definitions[\"room\"].only(\"id\")\n    end,\n\n    webhook_subscribe: lambda do |webhook_url, connection, input, recipe_id|\n      post(\"https://api.ciscospark.com/v1/webhooks\",\n           name: \"Workato recipe #{recipe_id}\",\n           targetUrl: webhook_url,\n           resource: \"messages\",\n           event: \"created\",\n           filter: \"roomId=#{input['id']}\")\n    end,\n\n    webhook_notification: lambda do |input, payload|\n      payload[\"data\"]\n    end,\n\n    webhook_unsubscribe: lambda do |webhook|\n      delete(\"https://api.ciscospark.com/v1/webhooks/#{webhook['id']}\")\n    end,\n\n    dedup: lambda do |message) {\n      message[\"id\"]\n    end,\n\n    output_fields: lambda do |object_definitions|\n      object_definitions[\"message\"]\n    end\n  }\n}\n```\n\n### webhook_subscribe\n\nThis block is called when the recipe is being started to run necessery API calls to subscribe for further webhook notifications. The endpoint that supposed to be register is passed in `webhook_url` parameter with other data that could be useful while registering the webhook.\n\n### webhook_unsubscribe\n\nThis block will be called after recipe start to unsubscribe from webhook notifications.\n\n### webhook_notification\n\nPOST/PUT HTTP requests can be used to notify about new trigger events. JSON-encoded payload is expected. This block is called to extract trigger output data from webhook notification payload (`payload` attribute). Original trigger input is also available in this block (`input` attribute)\n\n## Object Definition\n\nObject Definitions is an important component of the SDK. It allows you to define your schema for objects to be used in the actions and triggers. It allows you to easily define outputs and inputs later on.\n\n### Static Definition\n\nThe most basic way to build an object definition is to define the field name and type\n\n```ruby\nobject_definitions: {\n  push: {\n    fields: lambda do\n      [\n        { name: \"active\", type: :boolean },\n        { name: \"body\" },\n        { name: \"created\" },\n        { name: \"direction\" },\n        { name: \"dismissed\", type: :boolean },\n        { name: \"iden\" },\n        { name: \"modified\" },\n        { name: \"receiver_email\" },\n        { name: \"receiver_email_normalized\" },\n        { name: \"receiver_iden\" },\n        { name: \"sender_email\" },\n        { name: \"sender_email_normalized\" },\n        { name: \"sender_iden\" },\n        { name: \"sender_name\" },\n        { name: \"title\" },\n        { name: \"type\" },\n      ]\n    end\n  }\n}\n```\n\nIn this example, the object “Push” is being defined in the fields lambda literal.\n\nDefined as an array of objects. Each field object corresponds to a field in the comment object.\n\n### Dynamic Definition\n\n```ruby\nobject_definitions: {\n\n  form: {\n    fields: lambda do |connection|\n      get(\"https://api.unbounce.com/pages/#{connection['page_id']}/form_fields\")[\"formFields\"].\n        map { |field| { name: field[\"id\"] } }\n    end\n  }\n}\n```\n\n### Components\n\nKey | Definition\n--- | ----------\nname | The name of this field. For example `id` or `created_at`\ntype | The data type of this field. Default value is string\ncontrol_type | The input field type to expose in a recipe.\npick_list | If control type is 'select', this component is  required. See more in **Pick List**\nproperties | When defining nested objects, use the properties key to define the fields in the object. Remember to define the type as `:array` or `:object`\n\n#### type\nIt should be given the symbol notation (prepend colon)\n\nSupported types:\n`:string`, `:integer`, `:datetime`, `:date`, `:boolean`, `:object`, `:array`\n\n`:object`, and `:array` must be accompanied with properties\n\n#### control_type\n\nSome of the available values are\nurl: the data field will show a link\nselect: the data field will be a pick list (make sure to include the pick_list property\n\nOther supported types:\ntimestamp, checkbox, phone, email, text, number, text-area\n\n## Test\n\nTest endpoint to ensure that connection is successful.\n\nTypically, this should be a request that will always be accessible to any user.\n\nHere are some examples:\n\n```ruby\ntest: lambda do |connection|\n  get(\"https://person.clearbit.com/v1/people/email/eeshansim@gmail.com\")\nend\n```\n\n```ruby\ntest: lambda do |connection|\n  get(\"https://app.clicktime.com/api/1.3/session\")\nend\n```\n\n## Pick List\nA pick list is list of choices predefined for a user to select instead of having to input the actual values.\nIt is useful when there is a list of accepted values for a field or when the field requires a value that is not visible. For example, a field that requires User ID will require a pick list that displays the User names and pass the corresponding User's ID to that field.\n\n### Defining a pick list\nThere are 2 ways to define a pick list: dynamically or statically.\n\nStatic example:\n\nPick list is defined as a array of selections. Each selection is an array made up of 2 elements.\n\nThe first element in the selection array is the value displayed and the second element is the value of that selection.\n```ruby\npick_lists: {\n  folder: lambda do |connection|\n    [\n      # Display name, value\n      [\"Root\",\"111390\"],\n      [\"Recycle Bin\",\"235611\"]\n    ]\n  end\n}\n```\n\nDynamic example:\n```ruby\npick_lists: {\n  folder: lambda do |connection|\n    get(\"https://www.wrike.com/api/v3/folders\")[\"data\"].\n      map { |folder| [folder[\"title\"], folder[\"id\"]] }\n  end\n}\n```\nAfter making a GET requests for all folders available, the pick list is populated with folder `id`s and displays the corresponding folder `title`\n\n### Usage\n```ruby\ninput_fields: lambda do |object_definitions|\n  [\n    { name: \"folder_id\", control_type: \"select\", pick_list: \"folder\" }\n  ]\nend\n```\n\n## Configuration Fields\n\nOccassionally, input/output fields depend on user input. For example, the fields for an object depends on the chosen object. Here, we introduce `config_fields`. It is an optional key available in both actions and triggers. It is a special type of input field that can be used to generate other dependent input/output fields. We see this in the merge_document action in Webmerge.\n\n```ruby\naction: {\n  merge_document: {\n    config_fields: [\n      # this field shows up first in recipe as a picklist of documents to use\n      {\n        name: \"document_id\",\n        label: \"Document\",\n        control_type: :select,\n        pick_list: \"documents\",\n        optional: false\n      }\n    ],\n\n    input_fields: lambda do |object_definition|\n      object_definition[\"document\"]\n    end,\n\n    execute: lambda do |_connection, input|\n      d = input[\"document_id\"].split(\"|\")\n      post(d.last.to_s, input)\n    end,\n\n    output_fields: lambda do |_object_definition|\n      [\n        {\n          name: \"success\"\n        }\n      ]\n    end\n  }\n}\n```\n\nHere, the fields are different for each document, hence a user first chooses a document (from a pick list of documents available in the instance) which is then used to generate the remaining fields. This part is done by specifying that the \"document\" object definition is dependent on the value of \"document_id\" within config_field. Notice that the fields block accepts a second argument `config_fields`. This argument refers to the field(s) you define in `config_fields` in each action/trigger.\n\n```ruby\nobject_definition: {\n  document: {\n    fields: lambda do |_connection, config_fields|\n      return [] if config_fields.blank?\n      get(\"https://www.webmerge.me/api/documents/#{config_fields[\"document_id\"]}/fields\").\n        map { |field| field.slice(\"name\") }\n    end\n  }\n}\n```\n\nWhile the config_fields is empty, document objects will have no fields (empty array of fields). As soon as config_fields is given a value, a request is made to retrieve the fields present in this document. From a recipe user perspective, the action will appear initially as a single input with a list of documents. After selecting a document, the corresponding set of fields will be generated, to be used in the recipe.\n\n# Example Adapters\n\n## Basic Authentication Samples\n- [Harvest app connector](https://github.com/workato/custom_connector_docs/blob/master/custom_connectors/basic_auth/harvest_connector.rb)\n\n- [On-prem Security connector](https://github.com/workato/custom_connector_docs/blob/master/custom_connectors/basic_auth/onprem_security.rb)\n\n- [Freshdesk connector](https://github.com/workato/custom_connector_docs/blob/master/custom_connectors/basic_auth/freshdesk_connector.rb)\n\n- [Clearbit connector](https://github.com/workato/custom_connector_docs/blob/master/custom_connectors/basic_auth/clearbit_connector.rb)\n\n- [Close.io connector](https://github.com/workato/custom_connector_docs/blob/master/custom_connectors/basic_auth/close_io_connector.rb)\n\n- [Click Time connector](https://github.com/workato/custom_connector_docs/blob/master/custom_connectors/basic_auth/click_time_connector.rb)\n\n- [Toggl connector](https://github.com/workato/custom_connector_docs/blob/master/custom_connectors/basic_auth/toggl_connector.rb)\n\n- [Unbounce connector](https://github.com/workato/custom_connector_docs/blob/master/custom_connectors/basic_auth/unbounce_connector.rb)\n\n- [Watson Tone Analyzer connector](https://github.com/workato/custom_connector_docs/blob/master/custom_connectors/basic_auth/watson_tone_analyzer_connector.rb)\n\n- [Docparser connector](https://github.com/workato/custom_connector_docs/blob/master/custom_connectors/basic_auth/docparser_connector.rb)\n\n## OAuth2 Samples\n- [Podio connector](https://github.com/workato/custom_connector_docs/blob/master/custom_connectors/oauth2/podio_connector.rb)\n\n- [ProductHunt connector](https://github.com/workato/custom_connector_docs/blob/master/custom_connectors/oauth2/producthunt_connector.rb)\n\n- [Accelo connector](https://github.com/workato/custom_connector_docs/blob/master/custom_connectors/oauth2/accelo_connector.rb)\n\n- [Pushbullet connector](https://github.com/workato/custom_connector_docs/blob/master/custom_connectors/oauth2/pushbullet_connector.rb)\n\n- [Wrike connector](https://github.com/workato/custom_connector_docs/blob/master/custom_connectors/oauth2/wrike_connector.rb)\n\n- [Cisco Spark connector](https://github.com/workato/custom_connector_docs/blob/master/custom_connectors/oauth2/cisco_spark_connector.rb)\n\n- [AMcards connector](https://github.com/workato/custom_connector_docs/blob/master/custom_connectors/oauth2/amcards_connector.rb)\n\n- [Wachete connector](https://github.com/workato/custom_connector_docs/blob/master/custom_connectors/oauth2/wachete_connector.rb)\n\n## API Key Authentication Samples\n- [Gender API connector](https://github.com/workato/custom_connector_docs/blob/master/custom_connectors/api_key_auth/gender_api_connector.rb)\n\n- [Hipchat connector](https://github.com/workato/custom_connector_docs/blob/master/custom_connectors/custom_auth/hipchat_connector.rb)\n\n- [Codeship connector](https://github.com/workato/custom_connector_docs/blob/master/custom_connectors/api_key_auth/codeship_connector.rb)\n\n## Custom Authentication Samples\n- [LoJack app connector](https://github.com/workato/custom_connector_docs/blob/master/custom_connectors/custom_auth/lo_jack_connector.rb)\n\n- [SafetyCulture app connector](https://github.com/workato/custom_connector_docs/blob/master/custom_connectors/custom_auth/safetyculture_connector.rb)\n\n- [Knack HQ connector](https://github.com/workato/custom_connector_docs/blob/master/custom_connectors/custom_auth/knack_hq_connector.rb)\n\n- [Neto connector](https://github.com/workato/custom_connector_docs/blob/master/custom_connectors/custom_auth/neto_connector.rb)\n\n- [TSheets connector](https://github.com/workato/custom_connector_docs/blob/master/custom_connectors/custom_auth/tsheets_connector.rb)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fworkato%2Fcustom_connector_docs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fworkato%2Fcustom_connector_docs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fworkato%2Fcustom_connector_docs/lists"}