--- title: "App Platform Tutorial" slug: "app-platform-tutorial" description: "Learn how to build integrations using the App Platform with this comprehensive tutorial. From Go Templating to GraphQL, acquire the skills needed for success." updated: 2025-06-12T16:50:34Z published: 2025-06-12T16:50:34Z canonical: "help.gladly.com/app-platform-tutorial" --- > ## Documentation Index > Fetch the complete documentation index at: https://help.gladly.com/llms.txt > Use this file to discover all available pages before exploring further. # App Platform Tutorial This tutorial introduces many concepts and requires a few hours of learning before you will be equipped to write your own app. Please complete the following tutorials in the order they are written below. Many tools and technologies used here, such as **Go Templating** and **GraphQL (**see the [Before you start](https://help.gladly.com/developer-tutorials/docs/app-platform-tutorial#before-you-start) section) are widely used by many organizations. Take the time to search online for various tools and tutorials that exist if what we have provided leaves you with remaining questions. We intentionally chose widely available tools so that you could leverage publicly available educational materials. By the end of the tutorial, you will have a fully functional Gladly App that interacts with a fictitious order management system. Even if you are not familiar with GraphQL or Go Templating you will be able to build the app by following this tutorial. Remember that if you get stuck you can always compare your app with the finished version. It's available in our [app-platform-examples](https://github.com/gladly/app-platform-examples) Github repo - [sample_oms](https://github.com/gladly/app-platform-examples/tree/main/sample_oms). ## Before you start Install the CLI tool, then read the OMS API documentation. Both instructions are in the sections below. **Useful links** Example apps: [https://github.com/gladly/app-platform-appcfg-cli](https://github.com/gladly/app-platform-examples) CLI Documentation [https://github.com/gladly/app-platform-appcfg-cli/tree/main/docs](https://github.com/gladly/app-platform-appcfg-cli/tree/main/docs) ****What is GraphQL**** ### What is GraphQL GraphQL is a query language for APIs and a runtime for executing those queries with existing data. In short, we use GraphQL in App Platform to make rest calls. We only support one query entry point per query and generate queries automatically from within Gladly. GraphQL can execute queries or mutations. It allows clients to request only the needed data, making it efficient and flexible. At its core, GraphQL defines a schema that describes the capabilities of the API, including types of data and operations that can be performed. Clients can then send queries to the server specifying the exact data they require, and the server responds with JSON data matching the structure of the query. GraphQL supports introspection, allowing clients to discover the schema and explore available queries and types dynamically. With its declarative nature and strong typing, GraphQL promotes efficient communication between clients and servers, enabling more predictable and maintainable API interactions. ****What is Go Templating**** ### What is Go Templating The Go Templating language is a powerful tool for generating textual output. It allows you to create dynamic content by embedding logic directly within template files. Templates contain text interspersed with "actions" that control the template's logic. These actions range from simple variable substitutions to more complex control structures like conditionals and loops. See [https://pkg.go.dev/text/template](https://pkg.go.dev/text/template) to access the text/template package. ****Install External App CLI Setup**** ### Install External App CLI Setup **Useful links** Example apps: [https://github.com/gladly/app-platform-appcfg-cli](https://github.com/gladly/app-platform-examples) ![](https://connect.gladly.com/wp-content/uploads/2023/04/green-external-link-icon.svg) CLI Documentation [https://github.com/gladly/app-platform-appcfg-cli/tree/main/docs](https://github.com/gladly/app-platform-appcfg-cli/tree/main/docs) ![](https://connect.gladly.com/wp-content/uploads/2023/04/green-external-link-icon.svg) **To Install** 1. Download the newest file containing the latest data from [https://github.com/gladly/app-platform-appcfg-cli](https://github.com/gladly/app-platform-appcfg-cli/releases) ![](https://connect.gladly.com/wp-content/uploads/2023/04/green-external-link-icon.svg) for your development environment. 2. Place the executable in `~/.local/bin/` 3. add `alias appcfg=~/.local/bin/appcfg` to your `.zshrc` file 4. In your CLI Run: `sudo chmod a+x ~/.local/bin/appcfg` 5. In your CLI Run: `appcfg` - this might fail with an error similar to the one below: ![](https://connect.gladly.com/wp-content/uploads/2024/05/appcfg.png) If so, inn Mac OS, go to **System Settings > Privacy & Security > Approve external app in the settings**: ![](https://connect.gladly.com/wp-content/uploads/2024/05/securityappcfg.jpg) 6. In your CLI Run: `appcfg` - approve the message in the modal 7. ONE TIME ONLY. Create a space for your Gladly projects. For example, `mkdir ~/gladly/projects` 8. `cd ~/gladly/projects` 9. In your CLI run: `appcfg init --app-name tutorial --author ela.com --description "My first Gladly app"` Running the command will return **GLADLY_APP_CFG_ROOT** value which you'll use in the next step. 10. In your CLI Run: `export GLADLY_APP_CFG_ROOT=<value returned by previous command>` e.g. `export GLADLY_APP_CFG_ROOT=/Users/ela/gladly/projects/tutorial` 11. You can then run `appcfg -h` to see the help options. ****Sample OMS API**** ### Sample OMS API Familiarize yourself with the Sample OMS APIs and think about what we want to achieve with our new App. In this example, we will do both actions and data pulls. Our actions will be canceling an order, refunding an order, and refunding a line item from an order. We will pull data on customers, orders, and products. Use those requirements to understand the third-party system and consider which endpoints might be useful for us. Before writing your app, becoming well-versed in the external system is important. Understand the various APIs, data structures, request URLs, request and response bodies, and response codes. At the very minimum, take note of: - Auth instructions - Methods and URLs - Sample request and response payloads - Headers (including request signing) ## **Overview** The **example-oms** is a demo app designed to mimic the functionalities of a real Order Management System (OMS). Its primary purpose is to showcase the capabilities of the app platform and walk you through our demo app tutorial. The application provides typical OMS functionalities, although it operates with simulated data and does not interact with real-world systems. Implemented as a Next.js application deployed on Vercel, Fake-OMS-demo-test generates entities dynamically using the Faker.js library (https://fakerjs.dev/ [![](https://connect.gladly.com/wp-content/uploads/2023/04/green-external-link-icon.svg)](https://fakerjs.dev/)). The Faker.js library allows the creation of realistic yet fictitious data. Importantly, the random number generator in use is seeded with the ID of the requested entity. As a result, subsequent requests with the same ID should yield consistent data. When a client initiates a modification to an entity, the new state is stored in an LRU (Least Recently Used) cache. Subsequent fetch requests attempt to retrieve data from this cache before generating new data. However, it's essential to note that the LRU cache has a capped memory footprint. If numerous simultaneous changes occur, items may be evicted from the cache, potentially leading to unexpected behavior. We used token-based authentication in our sample OMS. This means you must include an `apiToken` key-value pair in the header of your requests (see examples below). In this example API, the token's value is not authenticated nor verified, so you may populate it with any value. ## Fetch Data Actions Use these actions to retrieve data from the API. Ensure that you include an `apiToken` in the header of your requests. ## Fetch Customer by Email ```shell curl -H "apiToken: "   ``` > [!NOTE] > When requesting a customer by email address, that email address is encoded using btoa and the result is used as the id. Since the emails (like all the other data) are generated using a random seed, the initially queried email address is manually inserted into the list of email addresses* (the reason for this note will become apparent in the next section Fetch Customer by ID ) ### **Response - Single Customer Object** ```json {  "id": "YW5kcmV3QGV4YW1wbGUub3Jn",  "fullName": "Lamont Russel",  "phones": [    "+644938853685"  ],  "emails": [    "andrew@example.org",    "Lamont.Russel@gmail.com",    "Lamont_Russel@hotmail.com",    "Lamont_Russel@gmail.com"  ],  "ltv": "$142.11" } ``` ### **Possible Errors** ```json { "status": "error", "message": "Error: emailAddress is required" } ``` ## **Fetch Customer by ID** Retrieve a customer by their ID. ```shell curl -H "apiToken: " ``` > [!NOTE] > Something interesting to note: the provided ID is decoded using the built-in atob function and then validated to see if the result is an email address, if so, after generating the random email addresses for this id, the decoded email address can be inserted into the list ### **Response** ```json {  "id": "YW5kcmV3QGV4YW1wbGUub3Jn",  "fullName": "Lamont Russel",  "phones": [    "+644938853685"  ],  "emails": [    "andrew@example.org",    "Lamont.Russel@gmail.com",    "Lamont_Russel@hotmail.com",    "Lamont_Russel@gmail.com"  ],  "ltv": "$142.11" } ``` ## **Fetch Orders by Customer ID** Retrieve orders by customer ID. ```shell curl -H "apiToken: " ``` ### **Response- List of Orders** ```json [ {    "id": "1f85eb0a-caa8-42ee-ad10-1dcfd09d01e2",    "orderNumber": "sGMtsfUf",    "status": "pending",    "shippingAddress": "64389 Sienna Village Apt. 296",    "billingAddress": "",    "orderDate": "2021-03-18T01:53:56.382Z",    "customerId": "YW5kcmV3QGV4YW1wbGUuY29t",    "lineItems": [      {        "id": "f9930951-ff95-4e34-8ce4-e7320e01f538",        "quantity": 2,        "productId": "ab30f03a-be65-4b97-81f6-8f3157b13fbc",        "status": "refunded"      },      {        "id": "8e8a31b8-75e0-4edc-8a98-98fd29a6a18a",        "quantity": 3,        "productId": "5d2ba910-fd1b-4939-8f57-f71199964e95",        "status": "refunded"      },      {        "id": "e6f76583-40ca-4284-9baa-518a8a4e52e5",        "quantity": 4,        "productId": "38af722b-14be-40f1-a7fd-26b84bb46d15",        "status": "pending"      }    ],    "shippingSpeed": "expedited",    "totalPrice": "697.00",    "shippingAndHandling": "816.00"  },  ... } ``` ### **Possible Errors** ```json { "status": "error", "message": "Error: customerId or emailAddress is required" } ``` ## **Fetch Orders by Customer Email Address** Retrieve orders by customer email address. ```shell curl -H "apiToken: " ``` ### **Response - List of Orders** ```json [ {    "id": "1f85eb0a-caa8-42ee-ad10-1dcfd09d01e2",    "orderNumber": "sGMtsfUf",    "status": "pending",    "shippingAddress": "64389 Sienna Village Apt. 296",    "billingAddress": "",    "orderDate": "2021-03-18T01:53:56.382Z",    "customerId": "YW5kcmV3QGV4YW1wbGUuY29t",    "lineItems": [      {        "id": "f9930951-ff95-4e34-8ce4-e7320e01f538",        "quantity": 2,        "productId": "ab30f03a-be65-4b97-81f6-8f3157b13fbc",        "status": "refunded"      },      {        "id": "8e8a31b8-75e0-4edc-8a98-98fd29a6a18a",        "quantity": 3,        "productId": "5d2ba910-fd1b-4939-8f57-f71199964e95",        "status": "refunded"      },      {        "id": "e6f76583-40ca-4284-9baa-518a8a4e52e5",        "quantity": 4,        "productId": "38af722b-14be-40f1-a7fd-26b84bb46d15",        "status": "pending"      }    ],    "shippingSpeed": "expedited",    "totalPrice": "697.00",    "shippingAndHandling": "816.00"  },  ... } ``` ### **Possible Errors** ```json { "status": "error", "message": "Error: customerId or emailAddress is required" } ``` ## **Fetch Order by ID** Retrieve an order by its ID. ```shell curl -H "apiToken: " ``` ### **Response** ```json {  "id": "ae3b06a1-39aa-4351-84cd-1eae728643f9",  "orderNumber": "BZcIoDCi",  "status": "refunded",  "shippingAddress": "503 S 4th Street Apt. 808",  "billingAddress": "825 Henry Street Suite 284",  "orderDate": "2019-09-02T00:53:56.135Z",  "customerId": "5fe529f1-271d-4ced-bab8-ec28c19c0fb6",  "lineItems": [    {      "id": "1fe7c456-aebb-46e0-b300-5dd675c3de3b",      "quantity": 2,      "productId": "5ccf268a-5718-441d-b642-2b67ea070552",      "status": "refunded"    }  ],  "shippingSpeed": "regular",  "totalPrice": "237.00",  "shippingAndHandling": "698.00" } ``` ## **Fetch Product by ID** Retrieve a product by its ID. ```shell curl -H "apiToken: " ``` ### **Response** ```json {  "id": "6422b67e-a070-4552-ae7a-600fb22d8242",  "name": "Rustic Plastic Pants",  "productType": "Fantastic",  "sku": "caAGQJxaHxOE" } ``` ## Modify Order Actions Use the following actions to modify orders. ### Cancel an Order 1. Only orders with `status: PENDING` can be canceled. 2. Since order data (like all data) is generated randomly, at least consistently, it may take several attempts with random IDs to find one that meets this condition. 3. A few IDs are known to work as of commit SHA: `2ebdfee85b5907f3ce821b0a576945d7d096d9aa` - 13413109987 - add more here when discovered 4. POST body must include `cancellationReason`. ```shell curl -X POST -H "apiToken: " -d '{ "cancellationReason": "garbage" }' ``` ### **Response Code 200** ```json { "status": "success" } ``` ### **Possible Errors - Response Code 400** ```json { "status": "error", "message": "cancellationReason is required" } ``` ```json { "status": "error", "message": "Error: can not cancel an order in state [the state of the order]" } ``` The next time you fetch the order, by id, the status of the line item and the order should be `CANCELLED`. ## **Refund an Order** 1. Only orders with `status: DELIVERED` or `status: SHIPPING` can be refunded. 2. Since order data (like all data) is generated randomly, at least consistently, it may take several attempts with random IDs to find one that meets this condition. 3. A few IDs are known to work as of commit SHA: `2ebdfee85b5907f3ce821b0a576945d7d096d9aa` - 13413109918 - add more when discovered 4. The request body must include a `refundReason`. ```shell curl -i -X POST -H "apiToken: anythingyouwanthere"   -d '{ "refundReason": "mispriced" }' ``` ### **Response Code 200** ```json { "status": "success"} ``` ### **Possible Errors - Response Code 200** ```json { "status": "error", "message": "refund is required" } ``` ```json { "status": "error", "message": "Error: can not refund an order in state [the state of the order]" } ``` The next time you fetch the order, by id, the status of the order and all of its line items should be `REFUNDED`. ## Return Line Items in an Order 1. Only line items with `status: DELIVERED` can be returned. 2. Since line item data (like all data) is generated randomly, but at least consistently, it may take several attempts with random IDs to find one that meets this condition. 3. A few IDs are known to work as of commit SHA: `2ebdfee85b5907f3ce821b0a576945d7d096d9aa` - 122340991j810 - add more when discovered 4. The request body should include an `orderId` and a list of `lineItemIds`. *Note that `orderId` is a string. ```shell curl -i -X POST -H "apiToken: "   -d '{ "orderId": "122340991j810", "lineItemIds": [ "5b8fccb7-b23f-490a-8f80-99ffc92ca64a"] }' ``` ## **Response code 200:** ```json { "status": "success"} ``` ## **Possible errors - response code 400:** ```json {"status":"error","message":"Error: missing orderId or lineItemIds"} {"status":"error","message":"Error: line item does not exist asdf"} {"status":"error","message":"Error: can not return an item with status undefined"} ``` The next time you fetch the order, by id, the status of the line item and the order should be `RETURN_INITIATED`. ## Sample Apps For your convenience, we have created a repository of some sample apps: [https://github.com/gladly/app-platform-examples](https://github.com/gladly/app-platform-examples)[.](https://github.com/gladly/app-platform-examples) In particular, [sample_oms](https://github.com/gladly/app-platform-examples/tree/main/sample_oms) connect to the Sample OMS API referenced above. In this tutorial, we will be replicating its functionality. Clone the example repo: ```plaintext git clone git@github.com:gladly/app-platform-examples.git ``` Use HTTPS or SSH to clone the examples repo: **HTTPS:** `git clone https://github.com/gladly/app-platform-examples.gitl` **SSH:** `git clone git@github.com:gladly/app-platform-examples.git` ## Summary A pre-requisite for this tutorial is that you have read through the documentation of our sample OMS noted above. Please familiarize yourself with the Sample OMS APIs before continuing. We will be using this as our third-party system. **appcfg** aims to help you create the files/folders needed for a Gladly App Platform App. Files will be created as placeholders that you can fill in manually. This process will require running some commands, manually updating files, performing validation and tests, and then running your app. Once the app is written, it must be installed in Gladly, configured, and activated. Writing an app without this tool is possible, but we built it to make the process easier for you. When you build an app on Gladly’s App Platform, know that you are responsible for the data returned, actions taken by the app, maintenance, and testing. We are simply providing you with the ability to integrate with external systems. Given that, we recommend you pull as little data as necessary and enable as few actions as necessary. Because apps must be backward compatible, adding rather than taking away is much easier. Start small and build up from there. This tutorial is meant to introduce you to basic App Platform concepts you can use when building your own app — it is not a comprehensive tutorial. The top-level folders and files in your app folder will resemble the following image. The [sample_oms](https://github.com/gladly/app-platform-examples/tree/main/sample_oms) app we are referencing already contains all of these files. Please refer there if you get lost. We will go through the motions of adding all of this relevant configuration yourself in the tutorial below: ![Directory structure showing app, actions, authentication, data, and manifest.json files.](https://cdn.us.document360.io/7047b671-c4f2-4df0-bb0a-b9b511fd2452/Images/Documentation/tutorial-directory.png?sv=2026-02-06&spr=https&st=2026-10-31T12%3A38%3A47Z&se=2026-10-31T13%3A38%3A47Z&sr=c&sp=r&sig=W1KMjyDMbNU2IDH70psSBCE5QOBWoNxNrjWGp%2F%2BZ2nw%3D) We will build a sample app against our example OMS Server, which will walk you through creating your own app. Please reference our [documentation](https://github.com/gladly/app-platform-appcfg-cli/tree/main/docs) for detailed information on any aspect of Gladly App Configuration. ## Getting Started 1. Before we begin configuring our own app, let’s quickly run a few `appcfg` commands and validate the existing example app provided to you in the [Sample OMS](https://github.com/gladly/app-platform-examples/tree/main/sample_oms). 2. Run the following commands from the `sample_oms` folder to validate the existing actions and data. This app should pass validation. If everything looks good, proceed to the next step. Otherwise, fix any existing errors or reach out to your CSM. This step tells us that the app we are replicating passes validation. If the app we build later does not pass validation, we should compare it against the example. - Export variable to point to app root: `export GLADLY_APP_CFG_ROOT=app` - Validate actions: `appcfg validate actions` - Validate data: `appcfg validate data` ```javascript # Set environment variable to point to app root echo "export GLADLY_APP_CFG_ROOT=$(pwd)/app" >> ~/.zshrc && source ~/.zshrc # Validate actions appcfg validate actions # Validate data appcfg validate data ``` > 💡 Tip: At any point during development, run `appcfg validate` to catch errors early. 3. Initialize Install CLI tool and Handle Permissions. - You must Install the CLI tool with the correct permissions and environment setup. ```javascript # Make the binary executable chmod +x appcfg # Move it to a location in your PATH mkdir -p ~/.local/bin/ mv appcfg ~/.local/bin/ ``` Ensure `~/.local/bin/` is in your PATH: - > 🔁 **Important**: Restart your terminal or run `source ~/.zshrc` to apply changes. - Add the following to your `.zshrc` (or `.bashrc` if using bash): `echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc` `source ~/.zshrc` - Common Permissions Issues: > 💡 Use `which appcfg` to verify installation. 1. macOS may block the app as an unidentified developer. If that happens: 1. Go to the `System Preferences > Security & Privacy` 2. Click “Allow Anyway” under the blocked app warning. 4. Initialize your App. - If you haven't already initialized your app in step **Install** **External App CLI Setup** [#](https://connect.gladly.com/docs/developer-tutorials/article/app-platform-tutorial/#h-install-external-app-cli-setup) do so now. From the root directory of your app run: `appcfg init -n <app_name> -a <org_name> -d <description>` e.g., `appcfg init --app-name tutorial --author ela.com --description "My first Gladly app"` Running the command will return **GLADLY_APP_CFG_ROOT** value which you'll use in the next step. - Export the root variable: `export GLADLY_APP_CFG_ROOT=<value returned by previous command>` e.g., `export GLADLY_APP_CFG_ROOT=/Users/ela/gladly/projects/tutorial` **Note** – Since we set the root app folder name, this will be the root of any work you do using the configuration tool unless you change the root app folder name. 5. Before continuing to configure our sample app, we want to identify data pull and action names and set them aside. In our example, we will start with our three data pulls `customers`, `orders`, and `products` , then move on to our three actions: `cancel_order`, `return_order`, and `refund_line_items`. We will start by pulling customer id by email address. 6. As you build your app, please refer to the example JSON in our test folders to understand the data's shape. This is meant to aid your discovery as you familiarize yourself with the app we will build on the App Platform. ### AUTHENTICATION Our sample OMS app does not need any action-specific or data pull-specific headers. They will all inherit our app-level auth headers. Fill out the following auth to run our app against the external system. In this example, we are configuring auth before data pulls/actions, but feel free to start the data pulls or actions first if that's how you prefer to think about things. #### **Adding Auth Header** Determine Auth Header name. In our case, we need to provide an `apiToken` key value pair. The command we will use is: `appcfg add auth-header {header name} [flags]` - Run the command: `appcfg add auth-header apiToken` - `apiToken.gtpl` contains <`{{.integration.secrets.apiToken}}` - `authentication/headers/_test_/data` folder contains two files: - `expected_apiToken.txt` contains `secret_token` - `integration.json` contains: ```javascript {  "configuration": {  },  "secrets": {    "apiToken": "secret_token"  } } ``` Place the file in: `tutorial/authentication/headers` Set up the test data in: `tutorial/authentication/headers/_test_/data/success/expected_apiToken.txt` Example content: `secret_token_value_here` Ensure additional test data files like `correlationId.json` are also correctly placed. Test the auth header by running `appcfg test auth-header`. If no error is returned, the tests have passed. ### DATA PULLS 1. Review the Sample OMS Server API docs again (in the [Before you start](https://help.gladly.com/developer-tutorials/docs/app-platform-tutorial#before-you-start) section) to confirm you understand the relationships between our three data pulls. Take your time to digest the structure of the external APIs. You’ll notice that `customer` does not have any dependent data, `orders` are dependent on the customer, and `products` are dependent on the order. This tutorial is meant to walk you through each data pull one by one. You will run through this tutorial three times, once for each data pull. 2. sRun the `add data-pull` command for our three desired data pulls (`customer`, `orders`, `products`). **Customer** is highlighted in **pink**, **Orders** is highlighted in **orange**, and **Products** is highlighted in **yellow**. Start with the Customer data pull, going through all the steps highlighted in **pink**. When you finalize that data pull at the end of this tutorial, return and run through the tutorial again for Orders and then a third time for Products. We recommend this iterative process to help you identify and fix errors by building data pulls on top of one another rather than simultaneously. But it is ultimately up to you to decide how you like to build apps going forward. Find an approach that works for you. Some steps are the same for all three data pulls and contain no highlight. 3. You can create multiple requests by outputting multiple bodies via the request body. This approach should be used when external data is requested via a POST. The start of each body is specified in the template by adding a “#body” marker on the line that immediately precedes the start of the body content. The marker can appear anywhere on the line making it possible to place it just above the actual body content for clarity. For simplicity, it is not necessary to add a “#body” marker to templates that only generate one body. For requests where only the request body changes, the request url template can simply generate one URL to be used with each request body. This will be the most common case for a POST. It is, however, possible to have a unique URL for each request body by generating as many URLs as there are request bodies. For example: ```javascript {{ range .customer.emailAddresses }}        #body        {            “emailAddress”: “{{.}}”        }        {{ end }} ``` #### **Add Data Pulls** `appcfg add data-pull <data-pull-name> -m <http-method> -t <data-type> -v <semantic-version> -d <dependent_data>` (example command). - **CUSTOMER**: `appcfg add data-pull customer -m GET -t sample_oms_customer -v 1.0` - **ORDERS**: `appcfg add data-pull orders -m GET -t sample_oms_order -v 1.0 -d sample_oms_customer` - **PRODUCTS**: `appcfg add data-pull products -m GET -t sample_oms_product -v 1.0 -d sample_oms_order` 1. Navigate to your root folder and confirm the appropriate folders and files have been created for your data pull above. See the list below. If all looks good, continue to the next step. - `request_url.gtpl` - `config.json` - `external_id.gtpl` - `external_parent_id.gtpl` - `external_updated_at.gtpl` - `response_transformation.gtpl` - `data_schema.graphql` (one file for all three data pulls will be filled in later) 2. Again, we do not run the `add data-pull-header` command for each data pull because we have no headers specific to a data pull. Our auth headers are generic and can be applied to all data pulls. We created an auth header that will apply to all of our apps’ REST calls in an earlier step. #### **Configuring the Data Pulls** 1. Let’s fill out the various configuration files in our new data pulls. As you know from earlier steps, each data pull has its own folder within `data/pull`. We will be referencing template data here. 2. Each data pull also has a `_test_/data` folder comprised of dataset folders. The customer and correlationId files come pre-loaded with sample data. These data set folders also contain our expected value files that we will fill out. This configuration tool allows us to use static data so that the outcome of our tests is deterministic. Normally, Gladly will provide customer data dynamically to the App Platform API requests. 3. Note that the `urlquery` go template function is available to you to escape url queries to be url-safe (for more information on Go Template Functions, run `appcfg platform template-functions`)We start at the root of the template data with a `.` , and then we select the `primaryEmailAddress` from the customer object. 4. We recommend validating and testing incrementally as each component of a data pull is created. For example, you would start by adding the request url below and then running `validate` command to ensure it passes validation. After that, you should set up the test file for `request_url` and then run the test. After you’ve tested your request url, move on to adding the external id and so on. ****CUSTOMER**** ### CUSTOMER (There is no `externalData.json` file here because the customer does not rely on any dependent data). Install the CLI tool, then read the OMS API documentation. Both instructions are in the sections below. ****data/pull/customer/request_url**** ```javascript https://gladly-sample-oms.vercel.app/api/customers?emailAddress={{urlquery .customer.primaryEmailAddress}} ``` ****data/pull/customer/request_url**** ```javascript https://gladly-sample-oms.vercel.app/api/customers?emailAddress={{urlquery .customer.primaryEmailAddress}} ``` ****data/pull/customer/request_url**** ```javascript https://gladly-sample-oms.vercel.app/api/customers?emailAddress={{urlquery .customer.primaryEmailAddress ``` ****data/pull/customer/_test_/data/base_case/expected_request_url**** ```javascript https://gladly-sample-oms.vercel.app/api/customers?emailAddress=primary%40email.com ``` **data/pull/customer/external_id** is **{{.id}}** - Note that Gladly requires IDs to be strings, and any other ID type must be formatted as a string at this stage. **data/pull/customer/_test_/data/base_case/expected_external_id** is **cHJpbWFyeUBlbWFpbC5jb20=}** ****data/pull/customer/_test_/data/base_case/rawData**** ```javascript {  "emails": [    "primary@email.com",    "Eldon_Beatty@hotmail.com",    "Eldon46@gmail.com"  ],  "fullName": "Eldon Beatty Sr.",  "id": "cHJpbWFyeUBlbWFpbC5jb20=","ltv": "$85.14",  "phones": [    "+84251648562"  ] } ``` ****ORDERS**** ### ORDERS **data/pull/orders/external_id** is **{{.id}}** ****orders/_test_/data/base_case/expected_external_id**** ```javascript 1aaa9504-e616-418f-b018-c72054574a60 bf676a4a-8d6c-4cb7-a994-3e516c691fd3 ``` ****orders/_test_/data/base_case/expected_external_parent_id**** ```javascript cHJpbWFyeUBlbWFpbC5jb20= cHJpbWFyeUBlbWFpbC5jb20= ``` ****data/pull/orders/request_url**** ```javascript https://gladly-sample-oms.vercel.app/api/customers?emailAddress={{urlquery .customer.primaryEmailAddress}} ``` ****orders/_test_/data/base_case/expected_request_url**** ```javascript https://gladly-sample-oms.vercel.app/api/orders?customerId=oms-customer-1 ``` ****orders/_test_/data/base_case/rawData**** ```javascript [  {    "billingAddress": "13986 Elm Street Apt. 174",    "customerId": "cHJpbWFyeUBlbWFpbC5jb20=","id": "1aaa9504-e616-418f-b018-c72054574a60",    "lineItems": [      {        "id": "ce6fd4f0-6ffa-4da7-aadb-520601e41098",        "productId": "38eb4d2a-f5c2-4048-a45a-a654ea42ec32",        "quantity": 2,        "status": "canceled"      },      {        "id": "b08186b9-40b3-4754-92d4-d78121122fe8",        "productId": "04bc3623-1212-4c3e-af1a-20f79c53a88a",        "quantity": 3,        "status": "delivered"      }    ],    "orderDate": "2024-01-16T15:32:39.826Z",    "orderNumber": "kqZzUCha",    "shippingAddress": "181 Torp Square Apt. 861",    "shippingAndHandling": "397.00",    "shippingSpeed": "expedited",    "status": "canceled",    "totalPrice": "320.00"  },  {    "billingAddress": "",    "customerId": "cHJpbWFyeUBlbWFpbC5jb20=","id": "bf676a4a-8d6c-4cb7-a994-3e516c691fd3",    "lineItems": [      {        "id": "c4ae0937-bb9a-48bb-ac85-d1a58a5ca82b",        "productId": "e7583d9b-30ef-4f5a-a131-a1b3a102d8be",        "quantity": 4,        "status": "delivered"      },      {        "id": "d63b1a24-c7c1-427c-bce9-f780a0c6e543",        "productId": "fb1e57e1-3816-4fb1-acfb-763edd83ab44",        "quantity": 2,        "status": "canceled"      },      {        "id": "e0a13a9f-fbce-4345-acd8-d1ada69825dc",        "productId": "2c8f223c-8bd7-448f-b4c8-b475c3ae87da",        "quantity": 4,        "status": "pending"      }    ],    "orderDate": "2020-04-13T01:55:17.525Z",    "orderNumber": "FsVOhVCg",    "shippingAddress": "3897 Medhurst Parkways Suite 440",    "shippingAndHandling": "158.00",    "shippingSpeed": "regular",    "status": "pending",    "totalPrice": "249.00"  } ] ``` ****orders/_test_/data/base_case/external_data**** ```javascript {  "sample_oms_customer": [    {      "id": "oms-customer-1"    }  ] } ``` ****PRODUCTS**** ### PRODUCTS **external_id** is **{{.id}}** **expected_external_id** is **04bc3623-1212-4c3e-af1a-20f79c53a88a** ****request_url**** ```javascript {{- range .externalData.sample_oms_order}} {{- range .lineItems -}} https://gladly-sample-oms.vercel.app/api/products/{{.productId}} {{end}} {{- end}} ``` ****expected_request**** Note that we have multiple expected requests because our sample order contains multiple products) We will expand on this later on. ```javascript https://gladly-sample-oms.vercel.app/api/products/38eb4d2a-f5c2-4048-a45a-a654ea42ec32 https://gladly-sample-oms.vercel.app/api/products/04bc3623-1212-4c3e-af1a-20f79c53a88a https://gladly-sample-oms.vercel.app/api/products/e7583d9b-30ef-4f5a-a131-a1b3a102d8be https://gladly-sample-oms.vercel.app/api/products/fb1e57e1-3816-4fb1-acfb-763edd83ab44 https://gladly-sample-oms.vercel.app/api/products/2c8f223c-8bd7-448f-b4c8-b475c3ae87da ``` ****external_data**** This file contains data needed to execute your current data pull, the dependent external data). The config tool will continue to add to this file as you create more dependent data. ```javascript {  "sample_oms_order": [    {      "billingAddress": "13986 Elm Street Apt. 174",      "customerId": "cHJpbWFyeUBlbWFpbC5jb20=","id": "1aaa9504-e616-418f-b018-c72054574a60",      "lineItems": [        {          "id": "ce6fd4f0-6ffa-4da7-aadb-520601e41098",          "productId": "38eb4d2a-f5c2-4048-a45a-a654ea42ec32",          "quantity": 2,          "status": "canceled"        },        {          "id": "b08186b9-40b3-4754-92d4-d78121122fe8",          "productId": "04bc3623-1212-4c3e-af1a-20f79c53a88a",          "quantity": 3,          "status": "delivered"        }      ],      "orderDate": "2024-01-16T15:32:39.826Z",      "orderNumber": "kqZzUCha",      "shippingAddress": "181 Torp Square Apt. 861",      "shippingAndHandling": "397.00",      "shippingSpeed": "expedited",      "status": "canceled",      "totalPrice": "320.00"    },    {      "billingAddress": "",      "customerId": "cHJpbWFyeUBlbWFpbC5jb20=","id": "bf676a4a-8d6c-4cb7-a994-3e516c691fd3",      "lineItems": [        {          "id": "c4ae0937-bb9a-48bb-ac85-d1a58a5ca82b",          "productId": "e7583d9b-30ef-4f5a-a131-a1b3a102d8be",          "quantity": 4,          "status": "delivered"        },        {          "id": "d63b1a24-c7c1-427c-bce9-f780a0c6e543",          "productId": "fb1e57e1-3816-4fb1-acfb-763edd83ab44",          "quantity": 2,          "status": "canceled"        },        {          "id": "e0a13a9f-fbce-4345-acd8-d1ada69825dc",          "productId": "2c8f223c-8bd7-448f-b4c8-b475c3ae87da",          "quantity": 4,          "status": "pending"        }      ],      "orderDate": "2020-04-13T01:55:17.525Z",      "orderNumber": "FsVOhVCg",      "shippingAddress": "3897 Medhurst Parkways Suite 440",      "shippingAndHandling": "158.00",      "shippingSpeed": "regular",      "status": "pending",      "totalPrice": "249.00"    }  ] } ``` ****rawData**** The raw data returned by the external system. ```javascript {  "id": "04bc3623-1212-4c3e-af1a-20f79c53a88a",  "name": "Small Metal Keyboard",  "productType": "Handmade",  "sku": "ADVJwwsrJCYX" } ``` Next, let's clean up our data pull and data set by deleting unnecessary files that are not relevant. For example, we do not need to request payloads for our GET data pulls or any transformations. We will delete such files as `response_transformation.gtpl` and `external_updated_at.gtpl`. #### **Validate Data Pulls** Before moving on to GraphQL schemas, let's validate and test our data pulls. In the prior step, you tested specific data pull tests: 1. Run `appcfg validate data` 2. You’ll notice that empty files will appear in the validator. Please delete any empty files that are still noted in the validator. 3. If the validator has any other issues, please fix them before testing our app. #### **Test Data Pulls** If the configuration you’ve written passes validation, it’s time to run our tests against the sample payloads again and confirm that everything works. - We have already set up our test expectations and tested data sets individually. Confirm all expectations are correctly noted in the `expected_...` files. - Run the following commands to test each data pull. Adding additional tests in data-set folders within `{data pull name}/_test_/data` is possible if you want to add additional test cases. Use the flag `-d` to name the specific test folder you want to use. In our case, because we only have one test data set called `success`, we will set the `-d` flag. - **appcfg test data-pull customer -d success** - **appcfg test data-pull orders -d success** - **appcfg test data-pull products -d success** After running the tests above, compare the actual output with the expected output. If there is a failure, you will see an error in the console. If there are any errors while running the test, read and resolve them before re-running the test. As an example, the expected test output for **appcfg test data-pull customer -t url** should look something like the following in your terminal. ```javascript data/pull/customer/request_url.gtpl ----------------------------------- https://gladly-sample-oms.vercel.app/api/customers?emailAddress=primary%40email.com ``` Once tests are passing, we will run our data pulls against the external system. ### Run Data Pulls ```javascript appcfg run data-pull -s '{"apiToken": "token"}' ``` ## **GraphQL Schema** It is time to think about the data transformations and our data schema. While we will not be doing any data transformations because the raw data returned by our sample OMS is returned exactly in the format we want; we will be modeling data in GraphQL in the way that makes the most sense for our business. We want two GraphQL query entry points, one for **customer** and one for **orders**. Using an online transformation tool to convert your raw JSON response to GraphQL is a nice place to start. From there, you can tweak and modify. You can also write your GraphQL schema by hand. Add the following GraphQL Schema to `data_schema.graphql`: - Note that we use GraphQL directives to reference the data pull the GraphQL datatype relates to. Each transformation needs to correspond to a data definition in GraphQL. ```javascript type Product @dataType(name: "sample_oms_product", version: "1.0") {  id: ID  name: String  productType: String  sku: String } type LineItem {  id: String  quantity: Int  product: Product @childIds(template: "{{.productId}}") (we return product object rather than product ID here)  status: String } type Order @dataType(name: "sample_oms_order", version: "1.0") {  id: ID  orderNumber: String  status: String  shippingAddress: String  billingAddress: String  orderDate: DateTime  customerId: String  shippingSpeed: String  totalPrice: Currency  shippingAndHandling: Currency  lineItems: [LineItem] } type Customer @dataType(name: "sample_oms_customer", version: "1.0") {    id: ID    emails: [String]    fullName: String    ltv: Currency    numberOfOrders: Int    phones: [String]    state: String    orders: Order @parentId(template: "{{.id}}") } type Query {    customer: Customer    orders: [Order] } ``` ### Note on GraphQL Types that we support: 1. All the GraphQL types from the spec (Int, String, etc) 2. Datetime 3. Currency 4. Object Type Definitions 5. Enum 6. Input Type ### Note on GraphQL Query Entry Points: We have chosen only to expose two GraphQL Query entry points here (**customer** and **orders**) even though many data objects are relevant to users in Gladly. GraphQL is so efficient and flexible that clients can request only the data they need in a query. This helps to avoid issues with both underfetching data and overfetching data. In our case, because all of our data is hierarchical under Customers, users can request whatever nested fields they need using our Customer or Orders Queries, and there is no need for any additional explicit queries, such as **lineItems** or **product**. ### **Note on GraphQL Directives:** We utilize only the built-in GraphQL directives(`@deprecated`, `@include`, and `@skip`) plus three additional directives below (`@dataType`, `@childIds`, `@parentId`). #### **STANDARD** GraphQL provides several built-in directives that are commonly used in GraphQL schemas. These directives include: 1. `@deprecated`: Indicates that a field or enum value is deprecated and should not be used. It takes an optional reason argument to explain why the field is deprecated. 2. `@include(if: Boolean)`: Conditionally includes a field in the response based on the value of the `if` argument. If `if` is true, the field is included; otherwise, it is omitted. 3. `@skip(if: Boolean)`: Conditionally skips a field in the response based on the value of the `if` argument. If `if` is true, the field is skipped; otherwise, it is included. These directives allow us to control the shape and content of GraphQL responses based on runtime conditions and deprecation status. #### **ADD-ON** We have added three additional directives for you to use: - `@dataType`: We use the `@dataType` directive to associate a GraphQL type with a given data type from a data pull in Gladly. For example, below, you will see the GraphQL type GladlyCustomer is associated with the `gladly_customer` data pull version `"1.0"` ```javascript type GladlyCustomer @dataType(name: "gladly_customer", version: "1.0") { ... } ``` - `@childIds`: This directive specifies how child entities are identified within a parent entity. It typically takes a template as an argument, which is used to construct the unique identifiers of child entities based on the parent entity’s data. For example, `@childIds(template: "{{.productId}}")` might indicate that each child entity’s ID is derived from the `productId` field of the parent entity. - `@parentId`: This directive identifies the parent entity associated with a child entity. It often takes a template argument as well, which is used to construct the unique identifier of the parent entity based on the child entity’s data. For instance, `@parentId(template: "{{.id}}")` could indicate that the parent entity’s ID is derived from the id field of the child entity. These directives help establish hierarchical relationships between different data types in GraphQL schemas, allowing for structured and efficient data retrieval. ### Validate and Run GraphQL 1. Once your GraphQL data schema has been written, it is time to validate and test it. 2. Start by running the `vּalidate` command. 3. If your data schema passes validation, please run each GraphQL entry point using the following command. ``` appcfg run data-graphql -d success -q customer -s '{"apiToken": "token"}' ``` ``` appcfg run data-graphql -d success -q orders -s '{"apiToken": "token"}' ``` ## ACTIONS In our example, we will define three actions: `cancel_order`, `refund_order`, and `return_line_items` in our Sample OMS Server. This will be similar to our data pull tutorial, but we will work on actions in the external system rather than data pulls. You will be reusing many concepts from the data pull section above. 1. Run the `add action` command for each of our actions. Each of them is a `POST` with `application/json content`. - `appcfg add action cancel_order -m POST -c application/json` - `appcfg add action refund_order -m POST -c application/json` - `appcfg add action return_line_items -m POST -c application/json` Now that the action folders and files have been created, let’s go in and add data and test data. Be sure to validate and test your actions as you incrementally add each component below: `app/actions/cancel_order` - `request_url` is `https://gladly-sample-oms.vercel.app/api/orders/{{urlquery .inputs.orderId}}/cancel` - `expected_request_url.txt` contains `https://gladly-sample-oms.vercel.app/api/orders/order1/cancel` - `request_body`: ```javascript {  "cancellationReason": "{{.inputs.reason}}" } ``` - `expected_request_body.txt` contains `{"cancellationReason": "broken"}` - `rawData.json` contains `{"status": "success"}` - `inputs.json`: ```javascript {    "orderId": "order1",    "reason": "broken" } ``` `app/actions/refund_order` - `request_url` is `https://gladly-sample-oms.vercel.app/api/orders/{{urlquery .inputs.orderId}}/refund` - `expected_request_url.txt` contains `https://gladly-sample-oms.vercel.app/api/orders/order1/cancel` - `request_body`: ```javascript {  "refundReason": "{{.inputs.reason}}" } ``` - `expected_request_body.txt` contains `{"refundReason": "broken"}` - `rawData.json` contains `{"status": "success"}` - `inputs.json`: ```javascript {    "orderId": "customer1",    "reason": "broken" } ``` `app/actions/return_line_items` - `request_url` is `https://gladly-sample-oms.vercel.app/api/return` - `expected_request_url.txt` contains `https://gladly-sample-oms.vercel.app/api/return` - `request_body`: ```javascript {  "orderId": "{{.inputs.orderId}}",  "lineItemIds": {{toJson .inputs.lineItemIds}} } ``` - `expected_request_body.txt` contains: ```javascript {    "orderId": "order1",    "lineItemIds": ["lineItemA", "lineItemB"] } ``` - `rawData.json` contains `{"status": "success"}` - `inputs.json`: ```javascript {    "orderId": "order1",    "lineItemIds": ["lineItemA", "lineItemB"] } ``` ### GraphQL Schema It is time to fill out the GraphQL schema for our actions. `app/actions/actions_schema.graphql`: ```javascript type Result {    status: String!    message: String } type Mutation {    cancelOrder(orderId: String!, reason: String!): Result @action(name: "cancel_order")    refundOrder(orderId: String!, reason: String!): Result @action(name: "refund_order")    returnLineItems(orderId: String!, lineItemIds: [String!]!): Result @action(name: "return_line_items") } ``` - Delete unnecessary files that are not relevant. Before we move on to running against the external app, validate and test our actions again. 1. Run `appcfg validate actions` 2. You’ll notice that empty files will appear in the validator. Delete any empty files that are still noted in the validator. 3. If the validator has any other issues, please fix them before testing our app. If the configuration you’ve written passes validation, it’s time to run our tests against the sample payloads and see if our actions work. Use the flag `-d` to name the specific test folder you want to use. Because we only have one test dataset, we will use the `-d success` flag to select it. 1. Run `appcfg test action cancel_order -d success` 2. Run `appcfg test action refund_order -d success ` 3. Run `appcfg test action return_line_items -d success` After running the tests above, compare the actual output with the expected output. If there are any errors while running the test, read and resolve them before re-running the test. The expected test output for `appcfg test action cancel_order -d success `should look something like the following in your terminal: ```javascript ========================== Test action "cancel_order" ========================== actions/cancel_order/request_url.gtpl ------------------------------------- https://gladly-sample-oms.vercel.app/api/orders/order1/cancel actions/cancel_order/request_body.gtpl -------------------------------------- {  "cancellationReason": "broken" } actions/cancel_order/response_transformation.gtpl ------------------------------------------------- ``` Once tests are passed, we will run our actions against the external system. ```javascript appcfg run action -s ‘{“apiToken”: “token”}’ ``` Then run our GraphQL mutations against the external system. ```javascript appcfg run action-graphql -d success -m cancelOrder -s ‘{“apiToken”: “token”}’ -i ‘{“orderId”: “stuff”, “reason”: “stuff”}’ ``` ```javascript appcfg run action-graphql -d success -m refundOrder -s ‘{“apiToken”: “token”}’ -i ‘{“orderId”: “stuff”, “reason”: “stuff”}’ ``` ```javascript appcfg run action-graphql -d success -m returnLineItems -s ‘{“apiToken”: “token”}’ -i ‘{“orderId”: “stuff”, “lineItemIds”: “stuff”}’ ``` ### Final Steps 1. Validate all or a subset of your configuration again (actions or data) or both. - Run `appcfg validate actions` - Run `appcfg validate data` 2. Generate markdown documentation for your new app. - Run `appcfg docs -d <root directory for the documentation tree>` 3. Create the app zip file containing all of the configurations you created above. - Decide the path of the app file you'd like to create. - Run `appcfg build -f <path of app file>` #### Install and Configure Your App See [Installing and Managing Apps](https://help.gladly.com/developer-tutorials/docs/installing-and-managing-apps) for instructions.