# Laminar

Welcome to Laminar's documentation page.

## About Laminar

Laminar is a low-code platform that allows customers to build and monitor bespoke integrations faster.

<figure><img src="https://2934663110-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeyfSRRmLEurvKZJcO0Tg%2Fuploads%2FnENBT7qGxvP5AyoIvSZ4%2FLaminar_Dashboard.jpeg?alt=media&amp;token=4e1239bb-a175-4684-9097-df1d2bb611e9" alt=""><figcaption><p>Laminar Dashboard</p></figcaption></figure>

## Breaking down the problem

Custom integrations are a deep engineering problem, but can be boiled down into three main functions:

1. API Requests (getting and sending data)
2. Data transformations (translating data between systems)
3. Control flow (if this then that)

Engineering teams normally build tooling to support these functionalities in their codebases:

1. Authentication Token storage (per API authentication)
2. API Endpoint descriptions (where to make requests)
3. Error Handling (logging and observability)
4. Retry Mechanisms (ways to ensure integrations succeed)

Laminar aggregates that tooling into one platform, reduces the boilerplate required to assemble integrations allowing customers to ship integrations faster and meet customers' demand.

## Watch it in action

{% embed url="<https://www.loom.com/share/0de24d82d5244baf918dcb45a95f239a?sid=e24c62f4-8c8b-45cd-8a5e-3be6b634e73a>" %}
Watch a platform overview
{% endembed %}

## Quick links

In a hurry? Here are some useful links:

{% content-ref url="/pages/jDVAQ5ThQpDbXlVT1XCx" %}
[Getting Started](/getting-started)
{% endcontent-ref %}

{% content-ref url="/pages/c7QkUoh5in3otDsvTMMM" %}
[Debugging an integration](/debugging-an-integration/workflow-executions-trace)
{% endcontent-ref %}

{% content-ref url="/pages/o7catevVg2louYjJ4HCB" %}
[Product Support](/get-in-touch/product-support)
{% endcontent-ref %}


# Overview

Laminar is a platform that allows customers to build and monitor bespoke integrations.

<figure><img src="https://2934663110-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeyfSRRmLEurvKZJcO0Tg%2Fuploads%2FnENBT7qGxvP5AyoIvSZ4%2FLaminar_Dashboard.jpeg?alt=media&amp;token=4e1239bb-a175-4684-9097-df1d2bb611e9" alt=""><figcaption><p>Laminar Dashboard</p></figcaption></figure>


# Getting Started

Build your first integration in less than 15 minutes

Let's build a simple order fulfillment integration. When a new order is placed, it gets automatically sent to a fulfilment centre for processing, and then the order status is updated.

## Create a New Workflow

1. Click "New Workflow"
2. Name it "Order Fulfillment"

{% embed url="<https://files.gitbook.com/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeyfSRRmLEurvKZJcO0Tg%2Fuploads%2FbKzWf6SHTmspiyNb9djX%2FCreate%20Workflow.mp4?alt=media&token=13eb8d7b-89a6-43d1-90ca-9e775f92935f>" %}

<details>

<summary>The workflow will receive as input order data in the following format (Click to expand)</summary>

<pre class="language-json" data-full-width="false"><code class="lang-json"><strong>{
</strong>  "id": 1,
  "orderNumber": "ORD-1751999768784",
  "customerName": "Sarah Johnson",
  "customerEmail": "sarah@example.com",
  "status": "pending",
  "total": "234.50",
  "shippingAddressLine1": "123 Main Street",
  "shippingAddressLine2": "Apt 4B",
  "shippingCity": "New York",
  "shippingState": "NY",
  "shippingZipCode": "10001",
  "shippingCountry": "US",
  "createdAt": "2025-01-08T18:36:08.000Z",
  "updatedAt": "2025-01-08T18:36:08.000Z",
  "items": [
    {
      "id": 1,
      "orderId": 1,
      "productId": 1,
      "quantity": 2,
      "price": "129.99",
      "product": {
        "id": 1,
        "name": "Wireless Earbuds Pro",
        "sku": "WEP-001",
        "category": "Electronics",
        "price": "129.99",
        "stock": 25,
        "status": "active",
        "description": "High-quality wireless earbuds",
        "imageUrl": "https://images.unsplash.com/photo-1572569511254-d8f925fe2cbb"
      }
    }
  ]
}
</code></pre>

</details>

## Adding Steps to Workflow

{% stepper %}
{% step %}

### Transforming incoming order data

First up, we'll need to create a new step to convert incoming order schema to fulfillment centre format.

{% tabs %}
{% tab title="Order Schema (Input)" %}

```json
{
  "id": 5,
  "orderNumber": "ORD-1752204924208",
  "customerName": "Lisa Chen",
  "customerEmail": "lisa.chen@email.com",
  "status": "pending",
  "total": "299.99",
  "shippingAddressLine1": "789 Oak Drive",
  "shippingAddressLine2": "Unit 5",
  "shippingCity": "Seattle",
  "shippingState": "WA",
  "shippingZipCode": "98101",
  "shippingCountry": "US",
  "trackingUrl": null,
  "shipmentReferenceId": null,
  "createdAt": "2025-07-11T03:35:24.196Z",
  "updatedAt": "2025-07-11T03:35:24.208Z",
  "items": [
    {
      "id": 9,
      "orderId": 5,
      "productId": 2,
      "quantity": 1,
      "price": 299.99,
      "product": {
        "id": 2,
        "name": "Smart Watch Series X",
        "sku": "SWX-002",
        "category": "Electronics",
        "price": 299.99,
        "stock": 12,
        "status": "active",
        "description": "Advanced smartwatch with health monitoring",
        "imageUrl": "https://images.unsplash.com/photo-1523275335684-37898b6baf30?ixlib=rb-4.0.3&auto=format&fit=crop&w=96&h=96",
        "createdAt": "2025-07-08T18:36:08.439Z",
        "updatedAt": "2025-07-08T18:36:08.446Z"
      }
    }
  ]
}
```

{% endtab %}

{% tab title="Order Schema (Fulfillment) " %}

```json
{
  "customer_name": "Lisa Chen",
  "customer_email": "lisa.chen@email.com",
  "customer_phone": "+1234567890",
  "shipping_address": {
    "street": "789 Oak Drive Unit 5",
    "city": "Seattle",
    "state": "WA",
    "zip": "98101",
    "country": "US"
  },
  "items": [
    {
      "sku": "SWX-002",
      "name": "Smart Watch Series X",
      "quantity": 1,
      "price": 299.99
    }
  ],
  "shipping_method": "standard"
}
```

{% endtab %}
{% endtabs %}

1. Click "Create New Step"
2. Name it "Transform incoming order data"
3. Select preferred language. For this tutorial, we will use Javascript
4. Click on the icon to select the step type. For this step, we'll keep `Data Transformation`&#x20;

Once the step is created, we can now start adding transformation logic to our step.

```javascript
(data) => {
    const inputOrder = data.input;

    // Use Array.map to transform the items array
    const transformedItems = inputOrder.items.map(item => ({
        sku: item.product.sku,
        name: item.product.name,
        quantity: item.quantity,
        price: parseFloat(item.price) // Convert price from string to number
    }));

    // Construct the final output object
    const outputOrder = {
        customer_name: inputOrder.customerName,
        customer_email: inputOrder.customerEmail,
        customer_phone: "+1234567890", // Placeholder as source data is missing
        shipping_address: {
            street: `${inputOrder.shippingAddressLine1 || ''} ${inputOrder.shippingAddressLine2 || ''}`.trim(),
            city: inputOrder.shippingCity,
            state: inputOrder.shippingState,
            zip: inputOrder.shippingZipCode,
            country: inputOrder.shippingCountry
        },
        items: transformedItems,
        shipping_method: "standard" // Default value as source data is missing
    };

    return outputOrder;
}
```

{% embed url="<https://files.gitbook.com/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeyfSRRmLEurvKZJcO0Tg%2Fuploads%2Fp2S0V3JgC455vPad6eC4%2FTransform%20Incoming%20Order%20Data.mp4?alt=media&token=37427bf1-68ee-4475-8fec-00ec851c3cfa>" %}
{% endstep %}

{% step %}

### Send Order to Fulfillment Centre

Next up, we'll create a new step to send the transformed order data to the fulfillment centre via API.

1. Click "Create New Step"
2. Name it "Send Order to Fulfillment Centre"
3. Click on the icon to select the step type. For this step, we'll select `HTTP Request`&#x20;

We will then write logic for sending order data to fulfillment centre via HTTP Request.

```javascript
(data) => {
    // The body of the request is the output from the previous step (step_1)
    const requestBody = data.step_1.data;

    return {
        "lam.httpRequest": {
            "method": "POST",
            "url": "https://shipping-logistics-loic21.replit.app/api/orders",
            "headers": {
                "Content-Type": "application/json"
            },
            "body": requestBody
        }
    };
}
```

{% embed url="<https://files.gitbook.com/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeyfSRRmLEurvKZJcO0Tg%2Fuploads%2FTWg8Av4nTdClHI4Kcyqg%2FSend%20order%20to%20fulfillment%20centre.mp4?alt=media&token=20f323cf-77f8-4c19-85c4-6232069cfcb7>" %}
{% endstep %}

{% step %}

### Update Order Status

Finally, we'll add a final step to update the order status.

1. Click "Create New Step"
2. Name it "Update order status"
3. Click on the icon to select the step type. For this step, we'll select `HTTP Request`&#x20;

We will add logic for updating order status via HTTP Request.

```javascript
(data) => {
    // Get the order ID from the initial workflow input
    const orderId = data.input.id;
    
    // Get the tracking URL and number from the response of the previous step (step_2)
    const tracking_url = data.step_2.response.tracking_url
    const tracking_number = data.step_2.response.tracking_number

    return {
        "lam.httpRequest": {
            "method": "PUT",
            "url": `https://order-track-pro-loic21.replit.app/api/orders/${orderId}`,
            "headers": {
                "Content-Type": "application/json"
            },
            "body": {
                "status": "Processing",
                "tracking_url": tracking_url,
                "shipment_reference_id": tracking_number,
            }
        }
    };
}
```

{% embed url="<https://files.gitbook.com/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeyfSRRmLEurvKZJcO0Tg%2Fuploads%2F939wf3Z4yJMhCbIlWoiW%2FUpdate%20order%20status%20and%20tracking%20details.mp4?alt=media&token=e1f524e3-068f-46cd-9c29-de061d8bcd7d>" %}
{% endstep %}
{% endstepper %}

## Run your Workflow

We are now ready to test our workflow. We will be using the input below to run our workflow

* Provide Test Input (Use test input below)
* Click Run Workflow Button

<details>

<summary>Order Test Input (Click to expand)</summary>

```json
{
  "id": 5,
  "orderNumber": "ORD-1752204924208",
  "customerName": "Lisa Chen",
  "customerEmail": "lisa.chen@email.com",
  "status": "pending",
  "total": "299.99",
  "shippingAddressLine1": "789 Oak Drive",
  "shippingAddressLine2": "Unit 5",
  "shippingCity": "Seattle",
  "shippingState": "WA",
  "shippingZipCode": "98101",
  "shippingCountry": "US",
  "trackingUrl": null,
  "shipmentReferenceId": null,
  "createdAt": "2025-07-11T03:35:24.196Z",
  "updatedAt": "2025-07-11T03:35:24.208Z",
  "items": [
    {
      "id": 9,
      "orderId": 5,
      "productId": 2,
      "quantity": 1,
      "price": 299.99,
      "product": {
        "id": 2,
        "name": "Smart Watch Series X",
        "sku": "SWX-002",
        "category": "Electronics",
        "price": 299.99,
        "stock": 12,
        "status": "active",
        "description": "Advanced smartwatch with health monitoring",
        "imageUrl": "https://images.unsplash.com/photo-1523275335684-37898b6baf30?ixlib=rb-4.0.3&auto=format&fit=crop&w=96&h=96",
        "createdAt": "2025-07-08T18:36:08.439Z",
        "updatedAt": "2025-07-08T18:36:08.446Z"
      }
    }
  ]
}
```

</details>

After running your workflow, you'll see a new entry in the workflow executions section:

* **Overall Status**: Success or failure indicator
* **Step Results**: Individual step outputs and execution logs
* **Data Flow**: How data moved between steps
* **Execution Time**: Performance metrics for each step

{% embed url="<https://files.gitbook.com/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeyfSRRmLEurvKZJcO0Tg%2Fuploads%2FmcMfr6ryy1VT4bVmiMp7%2FRun%20Workflow.mp4?alt=media&token=57571bd3-38fa-4fdb-a122-0a5ca446d014>" %}

Now that you've built your first workflow, let's dive deeper into creating more sophisticated integrations. Building effective workflows requires understanding how data flows between steps and mastering the different types of operations you can perform.


# Workflows

In Laminar, every integration starts with a workflow.

A workflow is a sequence of steps that represent business logic. Think of it as a pipeline where data flows from one step to the next. Each step receives all the accumulated data from previous steps, processes it, and adds its own output to the flow. This creates a rich context that subsequent steps can use for decision-making and processing.

## Creating Workflows

As seen in the Getting Started guide, creating a workflow is straightforward. When naming your workflows, consider using descriptive names that clearly indicate their purpose.

{% embed url="<https://files.gitbook.com/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeyfSRRmLEurvKZJcO0Tg%2Fuploads%2FbKzWf6SHTmspiyNb9djX%2FCreate%20Workflow.mp4?alt=media&token=13eb8d7b-89a6-43d1-90ca-9e775f92935f>" %}
Creating Workflow
{% endembed %}


# Steps

A workflow step is a function that handles a specific task execute in a specific order in the context of an integration. Steps are the fundamental building blocks of any Laminar workflow.

## Step Types

There are multiple types of steps a user can build in Laminar, each designed for specific tasks, such as HTTP requests, data transformations:

The supported flow types are:

* **HTTP Request:** used to send or receive data through REST APIs.
* **Data Transformation:** used to transform data to a desired format.
* **Shell Scripts**: used for interacting with command-line tools (ex: git, salesforce-cli, etc...) and file manipulations that are too difficult to achieve directly in JavaScript or Python

## Creating Steps

Similarly to workflows, creating steps is also straightforward.

<figure><img src="https://2934663110-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeyfSRRmLEurvKZJcO0Tg%2Fuploads%2Fc9d5XQrsxlXPRoTGJuZE%2FScreenshot%202025-07-10%20at%2011.58.53%E2%80%AFPM.png?alt=media&amp;token=634b39be-a864-4839-a02f-824d924b78fe" alt=""><figcaption><p>Creating workflow steps</p></figcaption></figure>


# Step Programs

Once a step is created, it needs to be programmed to execute the intended task.

In Laminar, step logic can be programmed in various languages including:

* Javascript
* Python

## Step Program Format

All step programs must follow a specific function signature to ensure proper execution and data flow.

### Javascript

In JavaScript, your program must be a lambda function that accepts one argument, `data`.

```javascript
(data) => {
    // Your code here to process 'data' and return an action or transformed data
    return {}; // Always return an object, even if empty
}
```

{% hint style="info" %}

#### Javascript Available Libraries

lodash (as \_) <sub>**\[Pre-loaded]**</sub>

date-fns (format, parseISO functions) <sub>**\[Pre-loaded]**</sub>
{% endhint %}

### Python

In Python, your program must be a regular function named `transform` that accepts one argument, `data`.

```python
def transform(data):
    # Your logic here to process 'data' and return an action or transformed data
    return {} # Always return an object, even if empty
```

{% hint style="info" %}

#### Python Available Libraries

json, datetime, math, statistics, collections, itertools, functools, re, copy, decimal, csv, io, dataclasses, typing, enum
{% endhint %}

{% hint style="warning" %}
Your program must always return a JSON-serializable object.
{% endhint %}

## Step Program Input (Global Workflow Object)

The `data` input of each step program is a ***Global Workflow Object (GWO)***.

The GWO is a special object that represents the cumulative input/output state of the workflow at the runtime of the current step.

To illustrate how the GWO works, suppose a workflow has 3 steps. The JSON below illustrates what each step program would receive as input:

```json
// Step 1 receives:
{
    "input": { /* your workflow input */ }
}

// Step 2 receives:
{
    "input": { /* your workflow input */ },
    "step_1": {
        "response": { /* if step 1 was HTTP request */ },
        "data": { /* if step 1 was transformation */ }
    }
}

// Step 3 receives:
{
    "input": { /* your workflow input */ },
    "step_1": { /* step 1 result */ },
    "step_2": { /* step 2 result */ }
}
```

**Example:** Let's take the order fulfillment workflow from [Getting Started](/getting-started). It has the following steps

{% stepper %}
{% step %}

### Transform incoming order data (Data Transformation)

As the very first step, the step program would receive as input the following object:

```json
{
    "input": { /* workflow input - a.k.a order data  */ }
}
```

{% endstep %}

{% step %}

### Send transformed order data to Fulfillment Centre (HTTP Request)

Next, the step program would receive:

```json
{
    "input": { /* the workflow input - a.k.a order data  */ },
    "step_1": {
        "data": { /* since step 1 was a transformation */ }
    }
}
```

{% endstep %}

{% step %}

### Update order status (HTTP Request)

Finally, the step program would receive:

```json
{
    "input": { /* the workflow input - a.k.a order data  */ },
    "step_1": {
        "data": { /* since step 1 was a transformation */ }
    },
    "step_2": {
        "response": { /* since step 2 was an HTTP Request */ }
    },
}
```

{% endstep %}
{% endstepper %}

## Step Program Output

As previously stated, step programs must always return a JSON-serializable object, regardless of the program language.

Laminar uses specific keywords within the object returned by your step program to define the type of action Laminar should perform.

### HTTP Step Types

When writing an HTTP Request step program, your program must include the `lam.httpRequest` keyword in the returned object and it must follow the structure below:

```json
{
    "lam.httpRequest": {
        "method": "GET|POST|PUT|DELETE|PATCH",
        "url": "String",
        "headers": "Object (optional)",
        "pathParams": "Object (optional)",
        "queryParams": "Object (optional)",
        "body": "Object (optional)",
        "multipart": {
            "fileFieldName": "String",
            "binaryDataId": "String"
        } /* (optional, for file uploads) */
    }
}
```

### Shell Step Types

When writing an Shell step program, your program must include the `lam.shell` keyword in the returned object and it must follow the structure below:

```json
{
    "lam.shell": {
        "script": "Bash script as a string",
        "environment": "Object (optional)",
        "timeout": "Integer in seconds (optional, default: 300)",
        "binaryDataIds": "Array of binary data IDs (optional). Typically used for file downloads from previous steps, use `data.step_N.response['lam.binaryDataId']`"
    }
}
```

### Data Transformation Step Types

For data transformations, no keywords are needed. You can return any JSON object.

## Editing and Saving Step Programs in Laminar

Writing step programs in Laminar is fairly straightforward.

1. Click on a step to expand. The expand action will reveal an editor
2. Write your step program
3. Save your changes

<figure><img src="https://2934663110-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeyfSRRmLEurvKZJcO0Tg%2Fuploads%2FVrudTlUqSJBzBXUwlmEd%2FScreenshot%202025-07-11%20at%2012.01.31%E2%80%AFAM.png?alt=media&amp;token=b2a53c14-75db-4d07-8100-766163fabd35" alt=""><figcaption><p>Step Programs</p></figcaption></figure>


# Spotlight: Build Workflows with AI

Laminar has an integrated AI Chatbot called *Lami* that can significantly accelerate your workflow creation process based on your natural language descriptions.

{% hint style="info" %}
When working with APIs, it is recommended to **attach OpenAPI specifications or Postman collections** as context. These provide the AI with invaluable context about the structure and capabilities of your target APIs, leading to much more accurate and relevant generated workflows and code.
{% endhint %}

## **How it Works**

1. Start a conversation with the AI Chatbot and simply **describe the workflow you want to build** in plain English (e.g., "I need to send incoming order to fulfillment centre and update the order status and tracking url").
2. The AI will analyze your request and generate a draft workflow, complete with steps and step program code snippets.
3. You can then review the AI's suggestions, **approve** them to add to your workflow, **reject** them, or **modify** them as needed.

{% embed url="<https://files.gitbook.com/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeyfSRRmLEurvKZJcO0Tg%2Fuploads%2Fd57guP2zF3os6ykiPv4B%2FBuild%20workflow%20with%20AI.mp4?alt=media&token=25ff008f-40d0-401a-943a-bc05c5ca817f>" %}


# Advanced

Learn more about some of our more advanced features


# Workflows

Learn about Workflows in Laminar

## Overview

Workflows in Laminar are sequences of flows that represent business logic. When building an integration in Laminar, workflows are used to group sets of flows together that perform a business operation.

Each workflow will run its flows in sequence, and will store a [Global Workflow Object](/building-an-integration/advanced/workflows/global-workflow-object) as part of its execution.

Workflows can also be conditionally [ended early](/building-an-integration/advanced/workflows/workflow-exit-points) or invoked [by other workflows](/building-an-integration/advanced/workflows/invoke-secondary-workflows).


# How to Build

Building Workflows

### Overview

Laminar workflows allow you to create sequences of API operations with data transformations. Each workflow consists of multiple flows that execute in order, with the ability to pass data between them using the Global Workflow Object.

### Key Components

#### 1. Configuration Store

The configuration store manages reusable properties and credentials:

* Store properties referenced via `{{props.propertyName}}`
* Manage authentication credentials for flows
* Create environment-specific configurations (dev/staging/prod)

Example configuration store:

```json
{
  "name": "Production Store",
  "workspaceId": 123,
  "properties": [
    {
      "key": "apiVersion",
      "value": "2.1"
    },
    {
      "key": "baseUrl", 
      "value": "https://api.store.com"
    }
  ],
  "flowCredentialPairs": [
    {
      "flowId": 456,
      "sourceAuthCredentialId": 789,
      "destinationAuthCredentialId": 101
    }
  ]
}
```

#### 2. Flow Programs

Flow programs are JavaScript/TypeScript functions that:

* Receive payloads with input data and previous step results
* Make HTTP requests using `lam.httpRequest` or `lam.httpRequests`
* Transform data between steps
* Control workflow execution using keywords

Example flow program:

```javascript
(payload) => {
  const { input } = payload;
  
  const httpDestination = {
    "method": "POST",
    "url": "{{props.baseUrl}}/orders/{{orderId}}/items",
    "pathParams": {
      "orderId": input.orderId
    },
    "headers": {
      "Content-Type": "application/json",
      "X-Api-Version": "{{props.apiVersion}}"
    },
    "body": {
      "quantity": input.quantity,
      "notes": input.notes
    }
  };

  return {
    "lam.httpRequest": httpDestination
  };
}
```

### Advanced Features

#### Parallel HTTP Requests

Make multiple HTTP requests simultaneously:

```javascript
(payload) => {
  const { input } = payload;
  
  return {
    "lam.httpRequests": [
      {
        "method": "GET",
        "url": "{{props.baseUrl}}/inventory/{{input.itemId}}",
        "pathParams": {
          "itemId": input.itemId
        }
      },
      {
        "method": "GET",
        "url": "{{props.baseUrl}}/pricing/{{input.itemId}}",
        "pathParams": {
          "itemId": input.itemId
        }
      }
    ]
  };
}
```

#### Conditional Workflow Exit

Exit workflows early based on conditions:

```javascript
(payload) => {
  const { step_1 } = payload;
  
  if (step_1.inventory.quantity < 1) {
    return {
      "lam.exit": true,
      "data": {
        "error": "Out of stock"
      }
    };
  }
  
  // Continue processing...
}
```

#### Async Workflow Execution

Trigger other workflows asynchronously:

```javascript
(payload) => {
  return {
    "lam.asyncExecute": [
      {
        "lam.workflowId": 789,
        "lam.payload": {
          "orderId": payload.input.orderId,
          "event": "ORDER_CREATED"
        }
      }
    ],
    "data": {
      "status": "processing"
    }
  };
}
```

### Best Practices

#### Configuration Management

* Store environment-specific values in configuration store
* Use `{{props.propertyName}}` syntax for references
* Keep sensitive data in configuration store

#### Error Handling

* Use try-catch blocks in transformations
* Return meaningful error messages
* Use `lam.exit` for critical errors

#### Data Flow

* Access previous step data using `step_X` notation
* Keep transformations focused and single-purpose
* Use meaningful names for flows and properties

#### Security

* Use separate configuration stores per environment
* Rotate credentials regularly
* Implement role-based access control for configuration stores


# Global Workflow Object

Learn about the GWO in Laminar

## Global Workflow Object Structure

<details>

<summary>View an example Global Workflow Object</summary>

```json
{
  "step_1": {
    "data": {
      "totalUsers": 4,
      "averageAge": 31.25,
      "totalItemsSold": 81,
      "users": [
        {
          "name": "Alice",
          "email": "alice@example.com",
          "totalSpent": 22
        },
        {
          "name": "Bob",
          "email": "bob@example.com",
          "totalSpent": 20
        },
        {
          "name": "Charlie",
          "email": "charlie@example.com",
          "totalSpent": 24
        }
      ]
    },
    "response": {
      "message": "Received POST request",
      "contents": {
        "totalUsers": 4,
        "averageAge": 31.25,
        "totalItemsSold": 81,
        "users": [
          {
            "name": "Alice",
            "email": "alice@example.com",
            "totalSpent": 22
          },
          {
            "name": "Bob",
            "email": "bob@example.com",
            "totalSpent": 20
          },
          {
            "name": "Charlie",
            "email": "charlie@example.com",
            "totalSpent": 24
          }
        ]
      }
    }
  },
  "step_2": {
    "data": {
      "lam.result": [
        {
          "lam.workflowId": 49,
          "lam.payload": {
            "lam.queryParams": {
              "email": "alice@example.com"
            },
            "lam.body": {
              "name": "Alice",
              "email": "alice@example.com",
              "totalSpent": 22
            }
          }
        },
        {
          "lam.workflowId": 49,
          "lam.payload": {
            "lam.queryParams": {
              "email": "charlie@example.com"
            },
            "lam.body": {
              "name": "Charlie",
              "email": "charlie@example.com",
              "totalSpent": 24
            }
          }
        }
      ]
    },
    "response": {}
  }
}
```

</details>

The Global Workflow Object is tailored to keep track of data transformations and responses throughout the workflow's execution:

* **Input** (optional): Initial data input passed via [POST request ](broken://pages/NDYlc2hgJScoYlXk42FV)if the workflow starts with a flow that has no source.
* **Flow Steps**: Each flow within the workflow contributes to this object, where each step might include:
  * **Data**: This part holds the results of data transformations by the flow.
  * **Response**: Stores the output or response received from the destination API after the data is processed and sent.

## Usage and Functionality

* **Sequential Execution**: Each flow accesses and possibly modifies the Global Workflow Object, allowing subsequent flows to utilize the transformed data and responses from previous steps.
* **Conditional Logic**: The object structure supports workflows where flows can be dynamically adjusted or skipped based on conditions evaluated during the workflow execution.


# HTTP Request Batching

Learn how to running multiple HTTP Requests in one workflow step

When building integrations, engineers may sometimes need to dispatch incoming batch data to different destinations. For example, a high-traffic online retail store receives hundreds of orders that need to be dispatched to different warehouses based on the delivery address through an API call that only accepts one order at a time in the payload.

Laminar solves that problem with the `lam.httpRequests` keyword in the step editor that allows users to make multiple HTTP requests in parallel.

```javascript
(payload) => {
  const { input } = payload;
  
  return {
    "lam.httpRequests": [
      {
        "method": "GET",
        "url": "{{props.baseUrl}}/send-to-warehouse/premium",
        "body": input.premiumOrders
      },
      {
        "method": "GET",
        "url": "{{props.baseUrl}}/send-to-warehouse/regular",
        "pathParams": {
          "itemId": input.regularOrders
        }
      }
    ]
  };
}
```


# Workflow Exit Points

Learn how to stop a workflow execution gracefully

When building integrations, you may wish to terminate a workflow early if some preconditions are not met.

Laminar solves this with the `lam.exit` keyword which can be used to stop a workflow execution and optionally return a payload providing more context on the exit.

```javascript
(payload) => {
  const { step_1 } = payload;
  if (step_1.inventory.quantity < 1) {
    return {
      "lam.exit": true,
      // payload to be returned
      "data": { 
        "error": "Out of stock"
      }
    };
  }
  // Continue with normal processing...

```


# Invoke Secondary Workflows

Learn how to invoke workflows from other workflows

You can trigger other workflows asynchronously from within a primary workflow using the keyword `lam.asyncExecute`, allowing for more complex automation scenarios without waiting for the secondary workflow to complete.

Imagine you have an e-commerce application with two workflows:

1. `process-new-order`: Handles new orders, inventory updates
2. `customer-communication`: Sends various emails to customers

When a new order comes in, you want to process it immediately but handle customer communications separately.

As a first step of the `process-new-order` workflow, you would invoke the `customer-communication` workflow as follows:

```javascript
return {
  "lam.asyncExecute": [
    {
      "lam.workflowId": 39, // customer-communication workflow id
      "lam.payload": { /* payload data */ }
    }
  ]
}
```

You can also invoke multiple workflows asynchronously by appending to the `lam.asyncExecute` array.


# Configurations

Learn how to use configurations in Laminar

A common pattern in building integrations is validating them across different settings, environments or even customer profiles. For example, you may want to validate an integration using test credentials. In Laminar, this is handled by *configuration stores*.

With configuration stores, users can create different configurations and choose a specific configuration to execute a workflow.

## 1. Creating a configuration

The first step is to create key-value configuration pairs

{% hint style="warning" %}
Configuration keys are case-sensitive.
{% endhint %}

{% @arcade/embed flowId="8TIt4BHaRNQaC0OGcFoz" url="<https://app.arcade.software/share/8TIt4BHaRNQaC0OGcFoz>" %}

## 2. Use configuration in workflow steps

When you need to use configuration variables within your workflow steps definition, use the following format:

```javascript
{{config.<configuration_key>}}
```

**Example:**

```javascript
(data) => {
  let transformed_data = data.step_1.response
  
  return {
    "lam.httpRequest": {
      "method": "POST",
      "url": "{{config.WAREHOUSE_API_URL}}/warehouse/orders",
      "headers": {
        "Content-Type": "application/json"
      },
      "body": transformed_data
    }
  };
}
```

## 3. Execute workflow with specific configuration

In order to execute a workflow with a specific configuration, you need to add the configuration id to the workflow execution url. Refer to the example below:

```bash
# Execute workflow for Customer A
# workflow_execution_url: https://api.laminar.run/workflow/execute/external/<workflow_id>?api_key=<api_key>

curl -X POST '<workflow_execution_url>&configuration_id=<configuration_id>' \
  -H 'Content-Type: application/json' \
  -d '{
    "data": "your workflow input"
  }'
```


# Flows

Learn about Flows in Laminar

## Overview

Flows in Laminar are functions that handle a specific task in the context of a business operation.

They can be programmed to run data transformations, execute http requests using Javascript. Each flow runs as part of a Workflow and has an execution order.

### Sequence of execution

Flows run the following sequence to complete:

1. Receive Input data (If applicable)
2. Performs task and updates the [Global Workflow Object](/building-an-integration/advanced/workflows/global-workflow-object).


# Flow Types

Learn about the various flow types that we support.

There are multiple workflow flow types, each designed for specific tasks, such as HTTP requests, data transformations.

The supported flow types are:

* [**HTTP Requests**](/building-an-integration/advanced/flows/flow-types/http-request)**:** used to send or receive data through REST APIs.
* [**General Transform**](/building-an-integration/advanced/flows/flow-types/data-transformations)**:** used to transform data to a desired format.


# HTTP Request

Learn about writing HTTP requests in Laminar Editor

## Basic Structure

In Laminar, to codify an HTTP request, you must update your flow program to return an object with the following format:

```javascript
{
  "lam.httpRequest": {
    "method": "POST", // (or "GET", "PUT", "DELETE")
    "url": "https://api.example.com/endpoint",
    "pathParams": { /* optional */ },
    "queryParams": { /* optional */ },
    "headers": { /* optional */ },
    "body": { /* optional */ }
  }
}
```

#### Examples

1. Let's start with a basic GET request to fetch users:

```javascript
(payload) => {
  const { input } = payload;
  
  return {
    "lam.httpRequest": {
      "method": "GET",
      "url": "https://api.example.com/users"
    }
  }
}
```

2. Now, let's create a user with a POST request:

```javascript
(payload) => {
  const { input } = payload;
  
  return {
    "lam.httpRequest": {
      "method": "POST",
      "url": "https://api.example.com/products",
      "body": {
        "name": "John Doe",
        "email": "john@example.com"
      }
    }
  }
}
```

## Optional Attributes

Now let's explore each optional attribute and how to use them effectively.

### Path Parameters

Path parameters are used to replace dynamic segments in your URL path.

**Example:**

```javascript
(payload) => {
  const { input } = payload;
  
  return {
    "lam.httpRequest": {
      "method": "GET",
      "url": "https://api.example.com/users/{userId}",
      "pathParams": {
        "userId": "12345"
      }
    }
  }
}
```

This will resolve to a request to `https://api.example.com/users/12345`.

### Query Parameters

Query parameters add a query string to your URL.

**Example:**

```javascript
(payload) => {
  const { input } = payload;
  
  return {
    "lam.httpRequest": {
      "method": "GET",
      "url": "https://api.example.com/users",
      "queryParams": {
        "page": 1,
        "limit": 10,
        "sort": "name"
      }
    }
  }
}
```

This will resolve to a request to `https://api.example.com/users?page=1&limit=10&sort=name`&#x20;

### Headers

Headers allow you to send additional information with your request.

**Example:**

```javascript
(payload) => {
  const { input } = payload;
  
  return {
    "lam.httpRequest": {
      "method": "GET",
      "url": "https://api.example.com/users",
      "headers": {
        "Authorization": "Bearer token123",
        "Content-Type": "application/json",
        "Accept-Language": "en-US"
      }
    }
  }
}
```

### Body

The body contains data sent with the request. It's typically used with POST, PUT, and PATCH methods.

**Example:**

```javascript
(payload) => {
  const { input } = payload;
  
  return {
    "lam.httpRequest": {
      "method": "POST",
      "url": "https://api.example.com/users",
      "headers": {
        "Content-Type": "application/json"
      },
      "body": {
        "name": "Jane Smith",
        "email": "jane@example.com",
        "role": "admin"
      }
    }
  }
}
```

## Multiple Requests

Laminar also supports sending multiple HTTP requests in a single operation. This is useful for batch processing or when you need to perform several related actions at once. See [HTTP Request Batching](/building-an-integration/advanced/workflows/http-request-batching).

You can make your batch requests more powerful by using dynamic path parameters. This is especially useful when working with lists of IDs:

```javascript
(payload) => {
  const { input } = payload;
  
  return userIds.map(userId => ({
  "lam.httpRequest": {
    "method": "GET",
    "url": "https://api.example.com/users/{userId}",
    "pathParams": {
      "userId": userId
    }
  }
}));
```


# Data Transformations

Learn about writing data transformations in Laminar Editor

Transformations allow you to modify data before or after making HTTP requests. They are written as JavaScript arrow functions and have access to specific libraries to help with data manipulation. The following imports are available:

```
* lodash as _
* date-fns: { format, parseISO }
```

### Example: Format User Names

Let's start with a basic transformation that formats user names to uppercase:

```javascript
// Transform user names to uppercase
(payload) => {
  const users = payload.step_2.response
  return users.map(user => ({
    ...user,
    name: user.name.toUpperCase()
  }));
}
```

### Example: Filter and Enrich Orders

This example filters out canceled orders and adds formatted date strings:

```javascript
// The `format` function is already available, no need to import
(payload) => {
  const orders = payload.input
  return orders
    .filter(order => order.status !== 'canceled')
    .map(order => ({
      ...order,
      formattedDate: format(new Date(order.createdAt), 'MMM dd, yyyy')
    }));
}
```


# Flow Runs

Learn about Flow Runs in Laminar

## Overview

Flow Runs in Laminar are instances of flow executions within workflows. They manage and track the transformation and transfer of data from input to transformed data.

Flow Runs are critical for debugging integrations in Laminar. They provide logs and status updates for each execution. They also store execution details, including:

* **Start and completion timestamps** to track the duration of data processing.
* **Status updates** to monitor the success or failure of the operations.
* **Execution logs** for JQ debugging.
* **Payloads, transformations, and responses** to verify and validate the data inputs and outputs of the transformations.


# Supported Languages

Learn about the languages supported by Laminar

Our platform currently supports two programming languages for data transformations:

## Python

When using Python, write your code as a function:

```python
def functionName(data):
    # Your flow logic here
    return result
```

## JavaScript

When using JavaScript, write your code as a named lambda function:

```javascript
(data) => {
    // Your flow logic here
    return result
}
```

### Available JavaScript Imports

The following libraries can be used directly when using JavaScript to edit flows without requiring additional import statements.

* **lodash** (as `_`)

```javascript
(data) => {
    // Group users by role
    const groupedUsers = _.groupBy(data.input.users, 'role');
    
    // Get only active users
    const activeUsers = _.filter(data.input.users, {isActive: true});
    
    return {
        groupedUsers,
        activeUsers
    };
}
```

* **date-fns**: `{ format, parseISO }`&#x20;

```javascript
(data) => {
    // Format dates in a user-friendly way
    const formattedUsers = data.users.map(user => ({
        ...user,
        joinedDate: format(parseISO(user.joinedAt), 'MMM dd, yyyy')
    }));
    
    return formattedUsers;
}
```


# Managing Notifications

Learn how to manage Laminar notifications

By default, when a workflow fails, Laminar automatically sends notifications through two channels:

* In-app notifications appear in the user's notification center
* Email notifications are sent to the user's registered email address

Users can customize their notification preferences to suit their needs.

{% hint style="warning" %}
Disabling both channels will completely suppress failure notifications for the workflow. This can be useful for non-critical workflows or during testing phases.
{% endhint %}

{% embed url="<https://app.arcade.software/share/fhMnFRAvwAkI9R8HETrj>" %}
Manage notifications
{% endembed %}


# Best Practices

Learn all the best practices to build simple integrations that scale

When designing workflows in Laminar, following these best practices will help you create solutions that are maintainable, secure, and performant as your integrations grow.

## Keep Workflows Focused

* Design each workflow to accomplish a single, well-defined purpose
* Break complex processes into multiple linked workflows instead of creating monolithic configurations
* Use [asynchronous workflow triggering](/building-an-integration/advanced/workflows/invoke-secondary-workflows) for independent sub-processes

## Implement Proper Error Handling

* Add validation steps at the beginning of workflows
* Add [exit points](/building-an-integration/advanced/workflows/workflow-exit-points) with descriptive messages for expected failure scenarios or inputs that don't meet requirements
* Design workflows to be idempotent whenever possible

## Credential Management

* Never hardcode credentials in workflow step programs
* Use [Laminar's configuration management system](/building-an-integration/advanced/configurations) for all sensitive information
* Rotate credentials regularly via configuration updates


# Integration Design

The design pattern behind Laminar's solution to custom integrations

## Designing integrations

An integration in Laminar is can be described as a sequences of Flows, which each can run the following:

1. Initial API Request (optional)
2. Data Transformations
3. Control Flow
4. Final API Request (optional)

## Building atomic integrations

Business logic for any integration can be expressed in terms of Workflows in Laminar. Each workflow fulfils specific business logic for an integration using its sequence of flows.

Flows carry out the requests and data transforms necessary to accomplish **atomic** integration objectives.

## Workflow execution lifecycle

<figure><img src="https://2934663110-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeyfSRRmLEurvKZJcO0Tg%2Fuploads%2FqmkEsArZVulkXwdBMqmO%2Fimage.png?alt=media&amp;token=1e3d494c-706d-453a-b053-ebd5b6b08450" alt=""><figcaption><p>Example workflow sequence with N number of Flows that run as part of the Workflow</p></figcaption></figure>


# API Requests

Making API requests in Laminar

## Overview

Laminar provides multiple ways to make API requests:

1. Using source/destination APIs in flows
2. Using the `httpRequest` keyword directly in transformations

## Source and Destination APIs

Each flow can specify:

* A **source** API to fetch data from at the start of the flow
* A **destination** API to send transformed data to at the end of the flow

These are configured using [API Descriptions](/running-an-integration/api-descriptions) and authenticated through the [Configuration Store](https://github.com/laminar-run/docs/blob/main/concepts/configurations.md).

Responses and data transformations are stored in the [Global Workflow Object](/building-an-integration/advanced/workflows/global-workflow-object) during execution, and individual API request inputs and outputs are stored in [Flow Runs](/building-an-integration/advanced/flows/flow-runs).

## Direct HTTP Requests

### Single Request

Use `lam.httpRequest` to make a single HTTP request within a transformation:

### JavaScript

```javascript
(payload) => {
  const { input } = payload;
  
  return {
    "lam.httpRequest": {
      "method": "POST",
      "url": "{{props.baseUrl}}/orders",
      "headers": {
        "Authorization": "Bearer {{props.apiKey}}",
        "Content-Type": "application/json"
      },
      "body": {
        "orderId": input.orderId,
        "items": input.items
      }
    }
  };
}
```

### JQ

```jq
.input |
{
  "lam.httpRequest": {
    "method": "POST",
    "url": "{{props.baseUrl}}/orders",
    "headers": {
      "Authorization": "Bearer {{props.apiKey}}",
      "Content-Type": "application/json"
    },
    "body": {
      "orderId": .orderId,
      "items": .items
    }
  }
}
```

### Parallel Requests

Use `lam.httpRequests` to make multiple requests in parallel:

### JavaScript

```javascript
(payload) => {
  const { input } = payload;
  
  return {
    "lam.httpRequests": input.items.map(item => ({
      "method": "GET",
      "url": "{{props.baseUrl}}/inventory/{{itemId}}",
      "pathParams": {
        "itemId": item.id
      }
    }))
  };
}
```

### JQ

```jq
.input.items |
{
  "lam.httpRequests": map({
    "method": "GET",
    "url": "{{props.baseUrl}}/inventory/{{itemId}}",
    "pathParams": {
      "itemId": .id
    }
  })
}
```

## Request Components

| Component  | Description                          | Required |
| ---------- | ------------------------------------ | -------- |
| method     | HTTP method (GET, POST, PUT, DELETE) | Yes      |
| url        | Request URL with optional templates  | Yes      |
| pathParams | URL path parameters                  | No       |
| headers    | HTTP headers                         | No       |
| body       | Request body                         | No       |

## Using Configuration Properties

Reference configuration properties in URLs and headers using `{{props.propertyName}}` syntax:

```javascript
{
  "url": "{{props.baseUrl}}/api/{{version}}/orders",
  "headers": {
    "Authorization": "Bearer {{props.apiKey}}"
  }
}
```

## Choosing an Approach

* Use **Source/Destination APIs** when:
  * You have a fixed API endpoint for input/output
  * You want to reuse API configurations
  * You need clear separation of data flow
* Use **HTTP Request Keywords** when:
  * You need dynamic endpoint selection
  * You want to make requests mid-flow
  * You need to make parallel requests

## Related

* [HTTP Request Keyword](https://github.com/laminar-run/docs/blob/main/platform/keywords/lam.httprequest.md)
* [HTTP Requests Keyword](https://github.com/laminar-run/docs/blob/main/platform/keywords/lam.httprequests.md)
* [API Descriptions](/running-an-integration/api-descriptions)
* [Configurations](https://github.com/laminar-run/docs/blob/main/concepts/configurations.md)


# Data Transformations

Transforming data in Laminar

## Running data transformations

Laminar's [Lam Language](broken://pages/HKxG0mHw1Qa5maH9rRwF) is built entirely on top of [JQ](https://jqlang.github.io/jq/). Lam is run on each flow execution and is a JSON data transformation language that can support extremely complex conditional data transformations.

Data transformations access keys via `dot` notation and are able to reference data retrieved or transformed by previous flows in a workflow using the [Global Workflow Object](/building-an-integration/advanced/workflows/global-workflow-object).

<details>

<summary>View a simple JQ example</summary>

{% code lineNumbers="true" %}

```jq
# Remove some fields
.data | map(select(.value > 5))
```

{% endcode %}

</details>

<details>

<summary>View a complex JQ example</summary>

{% code overflow="wrap" lineNumbers="true" %}

```jq
# Define safe_notes with robust checking for nested structures
def safe_notes:
  if .step_1.response.notes then
    [.step_1.response.notes[] | select(.custom_types != null and any(.custom_types[]; .note_custom_value == "0 = \"DELIVERY_SUCCESSFUL\""))]
  else
    []
  end;

# Extract image URLs from notes that have them
def images:
  if .step_1.response.notes then
    [.step_1.response.notes[] | 
     select(.upload_url? and .upload_url != "") | 
     {url: .upload_url, extension: .upload_extension}]
  else
    []
  end;

# Evaluate the detailed_event and contents of notes
if (.input.detailed_event == "completed" and (safe_notes | length > 0)) then
  # Output for confirmed completed status with valid delivery success note and image URLs
  {
    "lam.resolveThenExecute": {
      "lam.workflowId": 55,
      "lam.payload": {
        "images": (images),
        "data": {
            "TrackingNumber": .input.address_custom_data.barcode,
            "ScanLatitude": .step_1.response.visited_lat,
            "ScanLongitude": .step_1.response.visited_lng,
        }
      },
      "lam.result": {}
    }
  }
elif (.input.detailed_event == "completed" and (safe_notes | length == 0)) or .input.detailed_event == "skipped" or .input.detailed_event == "failed" then
  # Output for delivery issues or no valid delivery success note found
  {
    "WS_Key": "..",
    "WSKeyLevel": "Carrier",
    "CarrierId": 10204,
    "TrackingNumber": .input.address_custom_data.barcode,
    "StartDate": (now | strflocaltime("%Y-%m-%dT%H:%M:%S")),
    "EndDate": (now + 86400 | strflocaltime("%Y-%m-%dT%H:%M:%S")),
    "TrackingInfo": {
      "EstimatedDeliveryDate": (now + 86400 | strflocaltime("%Y-%m-%dT%H:%M:%S")),
      "DeliveryDate": (now + 86400 | strflocaltime("%Y-%m-%dT%H:%M:%S")),
      "Event": {
        "ScanDateTime": (now | strflocaltime("%Y-%m-%dT%H:%M:%S")),
        "Status": "DeliveryException",
        "Description": "Exception occurred during delivery",
        "AlternateLanguageCode": "English",
        "ScanCountry": "CA",
        "ScanState": "ON",
        "ScanCity": "...",
        "ScanPostal": "..."
      },
      "PerformanceState": 0
    },
    "UpdateTrackingInfoShipmentLevel": true
  }
elif .input.detailed_event == "loaded" then
  # Output for 'loaded' status
  {
    "WS_Key": "...",
    "WSKeyLevel": "Carrier",
    "CarrierId": 10204,
    "TrackingNumber": .input.address_custom_data.barcode,
    "StartDate": (now | strflocaltime("%Y-%m-%dT%H:%M:%S")),
    "EndDate": (now + 86400 | strflocaltime("%Y-%m-%dT%H:%M:%S")),
    "TrackingInfo": {
      "EstimatedDeliveryDate": (now + 86400 | strflocaltime("%Y-%m-%dT%H:%M:%S")),
      "DeliveryDate": (now + 86400 | strflocaltime("%Y-%m-%dT%H:%M:%S")),
      "Event": {
        "ScanDateTime": (now | strflocaltime("%Y-%m-%dT%H:%M:%S")),
        "Status": "OutForDelivery",
        "Description": "Package is out for delivery",
        "AlternateLanguageCode": "English",
        "ScanCountry": "CA",
        "ScanState": "ON",
        "ScanCity": "...",
        "ScanPostal": "..."
      },
      "PerformanceState": 0
    },
    "UpdateTrackingInfoShipmentLevel": true
  }
else
  { "lam.exit": true }
end
```

{% endcode %}

</details>

## Invoking Laminar Keywords

Having a data transformation output one of the specified [Laminar Keywords](/building-an-integration/keywords) allows for more complex control flow logic to take place.

This is done by formatting the output in a way that is recognizable to the Laminar system as specified by the [Laminar Keywords](/building-an-integration/keywords).

```jq
.input |
if ((.activity_type == "route-destination-status" or
     .activity_type == "update-destinations") and
    (.detailed_event? | tostring | test("skipped|failed|completed|loaded")) and
    (.order_id | length > 0))
then
{
  "lam.resolveThenExecute": {
    "lam.workflowId": 60,
    "lam.payload": .,
    "lam.result": { "status": "OK" }
  }
}
else 
    { "lam.exit": true } 
end
```


# Control Flow

Control flow in Laminar

## Control flow basics

Adding `if this then that` logic in Laminar can be done using either JavaScript or JQ. Here are examples in both languages:

### JavaScript Example

```javascript
(payload) => {
  const { input } = payload;
  
  if ((input.activity_type === "route-destination-status" || 
       input.activity_type === "update-destinations") && 
      /skipped|failed|completed|loaded/.test(String(input.detailed_event)) &&
      input.order_id?.length > 0) {
    return {
      "lam.resolveThenExecute": {
        "lam.workflowId": 60,
        "lam.payload": input,
        "lam.result": { "status": "OK" }
      }
    };
  } else {
    return { "lam.exit": true };
  }
}
```

### JQ Example

```jq
.input |
if ((.activity_type == "route-destination-status" or
     .activity_type == "update-destinations") and
    (.detailed_event? | tostring | test("skipped|failed|completed|loaded")) and
    (.order_id | length > 0))
then
{
  "lam.resolveThenExecute": {
    "lam.workflowId": 60,
    "lam.payload": .,
    "lam.result": { "status": "OK" }
  }
}
else 
    { "lam.exit": true } 
end
```

## Another Control Flow Example

### JavaScript Version

```javascript
(payload) => {
  const results = payload.step_1.response.results;
  
  if (results.length === 0) {
    return { "lam.exit": true };
  } else if (results.length === 1) {
    return { 
      "order_ids": [Number(results[0].order_id)]
    };
  } else {
    return { "lam.exit": true };
  }
}
```

### JQ Version

```jq
.step_1.response.results | 
if length == 0 then 
  { "lam.exit": true } 
elif length == 1 then 
  .[0] | { "order_ids": [(.order_id | tonumber)] }
else 
  { "lam.exit": true } 
end
```

## Laminar Keywords

Keywords allow workflows to be executed conditionally from within flows, making it easier to support complex control flow logic within custom integrations.


# Keywords

Learn about specific Keywords that can be used to run complex logic.

## Invoking the system

Laminar Keywords were built to allow for complex use cases to be handled within workflows.

Each flow within a workflow can invoke a keyword by transforming its data to a format that is represents a keyword and what data it expects.

## Use cases

Keywords can be used to:

1. Iterate over data and run a workflow with each element
2. Conditionally invoke workflows
3. Adjust flow destination request metadata (headers, query params, body) dynamically
4. Exit workflows early
5. Resolve requests from external APIs that invoked Laminar and asynchronously continue running workflows

&#x20;


# Requests

Learn more about making dynamic requests with the headers, queryParams, and body keywords.

## Invocation

Data transformation must output:

```jq
{
    "lam.headers:": { ... },
    "lam.queryParams": { ... },
    "lam.body": { ... }
}
```

| Key         | Description                                                              | Type              |
| ----------- | ------------------------------------------------------------------------ | ----------------- |
| headers     | The workflow ID to invoke after the current pending request is resolved. | Object (optional) |
| queryParams | The payload to pass the workflow we're invoking.                         | Object (optional) |
| body        | The result we're resolving the pending request with                      | Object (optional) |

## Functionality

The `headers`, `queryParams`, and `body` keywords can be used to dynamically assign request metadata for our **destination** request as part of our flow execution. We can use these specific keywords to make GET requests with dynamic query parameters or headers that depend on data we've retrieved and have in our [Global Workflow Object](/building-an-integration/advanced/workflows/global-workflow-object).

## Examples

### Making dynamic requests

In order to dynamically assign headers and query params to requests we're making on our **destination** API description, we output specific keywords:

```jq
.step_1.data.user |
map(
if .totalSpent > 20 then
{
    "lam.headers:": {
        "x-api-key": 12345
    },
    "lam.queryParams": {
        "email": .email
    },
    "lam.body": .
}
else
    empty
end
)
```

### Specifying the body

In the case we specify any of the `headers` or `queryParams` keys, Laminar **will not** send any body unless **explicitly specified** by the `body` key.&#x20;


# lam.resolveThenExecute

Learn about the resolveThenExecute keyword

## Functionality

The `resolveThenExecute` keyword resolves a pending request with the contents from `lam.result` and then executes a new workflow. This enables asynchronous workflow execution by immediately responding to webhook requests while continuing processing.

## Invocation

Data transformation must output:

```javascript
{
  "lam.resolveThenExecute": {
    "lam.workflowId": 52,
    "lam.payload": { /* payload data */ },
    "lam.result": { /* result data */ }
  }
}
```

| Key        | Description                                               | Type    |
| ---------- | --------------------------------------------------------- | ------- |
| workflowId | The workflow ID to invoke after resolving pending request | Integer |
| payload    | The payload to pass to the workflow                       | Object  |
| result     | The result to resolve the pending request with            | Object  |

## Examples

```javascript
(payload) => {
  const { step_2, input } = payload;
  const { Recipient, PackagesInfo } = input;
  
  const packages = PackagesInfo.Packages.map(pkg => ({
    address_1: Recipient.Address1,
    address_2: Recipient.Address2,
    address_city: Recipient.City,
    address_state_id: Recipient.CountryStateCode,
    address_country_id: Recipient.CountryCode,
    address_zip: Recipient.PostalCode,
    tracking_number: pkg.PreAssignTrackingNumber.TrackingNumber
  }));

  return {
    "lam.resolveThenExecute": {
      "lam.workflowId": 52,
      "lam.payload": {
        "packages": packages
      },
      "lam.result": step_2.data
    }
  };
}
```

> In this example we start workflow 52 with our packages array and resolve the pending request with our $result.


# lam.exit

Learn more about the exit keyword

## Functionality

The `exit` keyword acts as an early return for a workflow. If a flow outputs the `exit` keyword, Laminar will halt the workflow execution and any remaining steps will not be executed.

## Invocation

Data transformation must output:

```javascript
{
  "lam.exit": true
}
```

| Key  | Description                                | Type    |
| ---- | ------------------------------------------ | ------- |
| exit | Whether or not we should exit the workflow | Boolean |

## Examples

```javascript
(payload) => {
  const { step_1 } = payload;
  const results = step_1.response.results;
  
  if (results.length === 0 || results.length > 1) {
    return { "lam.exit": true };
  }
  
  return { 
    "order_ids": [Number(results[0].order_id)]
  };
}
```


# lam.execute

Learn more about the execute keyword

## Functionality

The `execute` keyword executes each specified workflow in the array with its corresponding payload **synchronously**. This blocks the current workflow's execution until all specified workflows complete.

## Invocation

Data transformation must output:

```javascript
{
  "lam.execute": [
    {
      "lam.workflowId": 39,
      "lam.payload": { /* payload data */ }
    }
  ]
}
```

| Key        | Description                             | Type                           |
| ---------- | --------------------------------------- | ------------------------------ |
| execute    | The workflows to execute with a payload | Array<{ workflowId, payload }> |
| workflowId | The workflow ID to invoke               | Integer                        |
| payload    | The payload to pass to the workflow     | Object                         |

## Examples

```javascript
(payload) => {
  const { input } = payload;
  const { Recipient, PackagesInfo } = input;
  
  const packagePayloads = PackagesInfo.Packages.map(pkg => ({
    "lam.workflowId": 39,
    "lam.payload": {
      address_1: Recipient.Address1,
      address_2: Recipient.Address2,
      EXT_FIELD_first_name: Recipient.Firstname,
      EXT_FIELD_last_name: Recipient.Name,
      EXT_FIELD_email: Recipient.Email,
      EXT_FIELD_phone: Recipient.Tel,
      EXT_FIELD_weight: pkg.Weight.Value,
      custom_user_fields: [
        { order_custom_field_id: 1852 }
      ],
      address_city: Recipient.City,
      address_state_id: Recipient.CountryStateCode,
      address_country_id: Recipient.CountryCode,
      address_zip: Recipient.PostalCode,
      tracking_number: pkg.PreAssignTrackingNumber.TrackingNumber
    }
  }));

  return {
    "lam.execute": packagePayloads
  };
}
```

> In this example we map an array of packages to the `execute` keyword. Laminar will then invoke workflow 39 with each individual payload in sequence.


# lam.asyncExecute

Learn more about the asyncExecute keyword

## Functionality

The `asyncExecute` keyword executes each specified workflow in parallel. Unlike `lam.execute`, this keyword runs workflows **asynchronously** and does not block the current workflow's execution.

## Invocation

Data transformation must output:

```javascript
{
  "lam.asyncExecute": [
    {
      "lam.workflowId": 39,
      "lam.payload": { /* payload data */ }
    }
  ]
}
```

| Key          | Description                              | Type                           |
| ------------ | ---------------------------------------- | ------------------------------ |
| asyncExecute | The workflows to execute with a payload  | Array<{ workflowId, payload }> |
| workflowId   | The workflow ID to invoke asynchronously | Integer                        |
| payload      | The payload to pass to the workflow      | Object                         |

## Examples

```javascript
(payload) => {
  const { input } = payload;
  const { Recipient, PackagesInfo } = input;
  
  const packagePayloads = PackagesInfo.Packages.map(pkg => ({
    "lam.workflowId": 39,
    "lam.payload": {
      address_1: Recipient.Address1,
      address_2: Recipient.Address2,
      EXT_FIELD_first_name: Recipient.Firstname,
      EXT_FIELD_last_name: Recipient.Name,
      EXT_FIELD_email: Recipient.Email,
      EXT_FIELD_phone: Recipient.Tel,
      EXT_FIELD_weight: pkg.Weight.Value,
      custom_user_fields: [
        { order_custom_field_id: 1852 }
      ],
      address_city: Recipient.City,
      address_state_id: Recipient.CountryStateCode,
      address_country_id: Recipient.CountryCode,
      address_zip: Recipient.PostalCode,
      tracking_number: pkg.PreAssignTrackingNumber.TrackingNumber
    }
  }));

  return {
    "lam.asyncExecute": packagePayloads
  };
}
```

> In this example we map an array of packages to the `asyncExecute` keyword. Laminar will then invoke workflow 39 with each individual payload in parallel.


# \[DEPRECATED] lam.result

Learn more about the result keyword

{% hint style="danger" %}
This keyword has been deprecated and will not be supported in future versions of Laminar. Please use the [execute](broken://pages/jPTzXZQP9mhF5WUnmwrY) keyword instead.
{% endhint %}

## Invocation

Data transformation for a flow must output:

```jq
{ 
 "lam.result": [
  {
    "lam.workflowId": Integer,
    "lam.payload": { ... }
  }
 ]
}
```

| Key        | Description                                      | Type                                  |
| ---------- | ------------------------------------------------ | ------------------------------------- |
| result     | The workflows to execute with a payload.         | Array { lam.workflowId, lam.payload } |
| workflowId | The workflow ID to invoke.                       | Integer                               |
| payload    | The payload to pass the workflow we're invoking. | Object                                |

## Functionality

The `result` keyword will execute each specified workflow in the`lam.result` array with its corresponding payload. This keyword allows for iterative operations (i.e. running a `POST` request for various objects in a list) that should be run asynchronously and will not block the current running workflow's execution.&#x20;

## Examples

```jq
.input |
.ShipmentDetails.PackagesInfo.Packages as $pkgsInfo |
$pkgsInfo | to_entries | map(
    {
    "lam.workflowId": 53,
    "lam.payload": .value
    }
)
```

### Explanation

Note: if we output an array of JSON objects from a data transformation, the Lam compiler will automatically group each outputted element in a `lam.result` array in order to store one JSON output.

In this example, we're making use of this functionality by outputting several JSON objects in the form:

```json
{
    "lam.workflowId": 53,
    "lam.payload": {...}
}
```

That will then be collected under the `lam.result` key:

```json
{
  "lam.result": [
    {
      "lam.workflowId": 53,
      "lam.payload": {...}
    }
    ...
  ]
}
```

Which is how we're invoking this keyword.


# lam.httpRequest

Learn more about the httpRequest Keyword

## Functionality

The `httpRequest` keyword makes an HTTP request to the specified endpoint with the given parameters. The response will be stored in the flow's response object. For multiple parallel requests, see [lam.httpRequests](/building-an-integration/keywords/lam.httprequests).

### Invocation

Data transformation must output:

```javascript
{
  "lam.httpRequest": {
    "method": "POST",
    "url": "https://api.example.com/endpoint",
    "pathParams": { /* optional */ },
    "queryParams": { /* optional */ },
    "headers": { /* optional */ },
    "body": { /* optional */ }
  }
}
```

| Key         | Description                          | Type   | Required |
| ----------- | ------------------------------------ | ------ | -------- |
| method      | HTTP method (GET, POST, PUT, DELETE) | String | Yes      |
| url         | Destination URL                      | String | Yes      |
| pathParams  | URL path parameters                  | Object | No       |
| queryParams | URL query parameters                 | Object | No       |
| headers     | HTTP headers                         | Object | No       |
| body        | Request body                         | Object | No       |

### Examples

```javascript
(payload) => {
  const { input } = payload;
  
  return {
    "lam.httpRequest": {
      "method": "POST",
      "url": "{{props.baseUrl}}/orders/{{orderId}}/items",
      "pathParams": {
        "orderId": input.orderId
      },
      "headers": {
        "Content-Type": "application/json",
        "Authorization": "Bearer {{props.apiKey}}"
      },
      "body": {
        "quantity": input.quantity,
        "notes": input.notes
      }
    }
  };
}
```


# lam.httpRequests

Learn more about the httpRequests Keyword

## Functionality

The `httpRequests` keyword makes multiple HTTP requests in parallel. Each request in the array follows the same format as `lam.httpRequest`. The responses will be stored in the flow's response object in the same order as the requests.

### Invocation

Data transformation must output:

```javascript
{
  "lam.httpRequests": [
    {
      "method": "GET",
      "url": "https://api.example.com/inventory/{{itemId}}",
      "pathParams": { "itemId": "123" }
    },
    {
      "method": "GET", 
      "url": "https://api.example.com/pricing/{{itemId}}",
      "pathParams": { "itemId": "123" }
    }
  ]
}
```

| Key         | Description                          | Type   | Required |
| ----------- | ------------------------------------ | ------ | -------- |
| method      | HTTP method (GET, POST, PUT, DELETE) | String | Yes      |
| url         | Destination URL                      | String | Yes      |
| pathParams  | URL path parameters                  | Object | No       |
| queryParams | URL query parameters                 | Object | No       |
| headers     | HTTP headers                         | Object | No       |
| body        | Request body                         | Object | No       |

### Examples

```javascript
(payload) => {
  const { input } = payload;
  
  return {
    "lam.httpRequests": input.items.map(item => ({
      "method": "GET",
      "url": "{{props.baseUrl}}/items/{{itemId}}",
      "pathParams": {
        "itemId": item.id
      },
      "headers": {
        "Authorization": "Bearer {{props.apiKey}}"
      }
    }))
  };
}
```


# Languages

Learn about Languages in Laminar

### Overview

Laminar supports both JavaScript and JQ for writing flow transformations. Each language has its own strengths and use cases.

### JavaScript

JavaScript transformations use a simple function pattern:

```javascript
(payload) => {
  // Access data from previous steps or input
  const { input, step_1 } = payload;
  
  // Perform transformations
  const transformed = {
    // ... transformation logic
  };
  
  // Return result object
  return {
    "data": transformed,
    // Optional Laminar keywords
    "lam.httpRequest": {/*...*/}
  };
}
```

#### Advantages

* Familiar syntax for most developers
* Rich ecosystem of npm packages
* Native JSON handling
* Powerful array/object manipulation

#### Example Transformation

```javascript
(payload) => {
  const { step_1 } = payload;
  
  // Filter users who spent over $20
  const highValueUsers = step_1.data.users
    .filter(user => user.totalSpent > 20)
    .map(user => ({
      "lam.workflowId": 49,
      "lam.payload": {
        "lam.queryParams": {
          "email": user.email
        },
        "lam.body": user
      }
    }));

  return {
    "lam.result": highValueUsers
  };
}
```

### JQ

JQ is a lightweight, powerful language specifically designed for JSON processing:

```jq
# Access input data using dot notation
.input | 
# Transform using JQ operators
map(select(.totalSpent > 20)) |
# Output transformed data
{
  "lam.result": .
}
```

#### Advantages

* Concise syntax for JSON manipulation
* Powerful built-in functions
* Excellent performance for JSON processing
* Native streaming support

#### Example Transformation

```jq
.step_1.data.users |
map(
  select(.totalSpent > 20) |
  {
    "lam.workflowId": 49,
    "lam.payload": {
      "lam.queryParams": {
        "email": .email
      },
      "lam.body": .
    }
  }
)
```

### Choosing a Language

* Choose **JavaScript** when:
  * You need complex business logic
  * Your team is more familiar with JavaScript
  * You want to use npm packages
  * You need advanced string/array operations
* Choose **JQ** when:
  * You're primarily doing JSON transformation
  * You want concise, readable transformations
  * Performance is critical
  * You're working with streaming data


# In-App Workflow Execution

Laminar offers several flexible ways to trigger its execution. The simplest way is manually, directly from the Laminar user interface. This is ideal for testing or one-off tasks.

**Process:**

* Navigate to the specific workflow you wish to run.
* If your workflow expects an input, provide the necessary JSON payload.
* Click the **"Run Workflow"** button (found at the top of the workflow editor).

<figure><img src="https://2934663110-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeyfSRRmLEurvKZJcO0Tg%2Fuploads%2FAQFViAwyvEOScyvLaeav%2FScreenshot%202025-07-11%20at%208.21.52%E2%80%AFAM.png?alt=media&amp;token=09c19e4c-2aa8-4013-b098-1fffa386d4ce" alt=""><figcaption></figcaption></figure>


# Remote Workflow Execution

Laminar allows you to trigger workflows via a unique API execution link. This enables seamless integration of Laminar workflows into your existing applications, scripts, or CI/CD pipelines.

You can find the execution URL in the workflow editor controls:

1. Locate textbox under <kbd>TRIGGER URL & SCHEDULES</kbd> at the top right of the editor
2. Click the copy button to copy the execution link.

{% embed url="<https://files.gitbook.com/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeyfSRRmLEurvKZJcO0Tg%2Fuploads%2Fo8OKvxizuGBYihuEOnyG%2FCopy%20execution%20url.mp4?alt=media&token=d181d9f0-66fa-4ed4-b36b-be7cd413caeb>" %}

Once you have the link, follow the API spec below to make your HTTP Request.

## Execute Workflow (External)

> Executes a workflow using external API key authentication

```json
{"openapi":"3.0.1","info":{"title":"Laminar API","version":"v1"},"tags":[{"name":"Workflows","description":"Laminar Workflow endpoints, for managing workflows and workflow executions."}],"servers":[{"url":"https://api.laminar.run","description":"Laminar API"}],"security":[{"api-key-query":[]},{"api-key":[]}],"components":{"securitySchemes":{"api-key-query":{"type":"apiKey","description":"API key authentication via query parameter","name":"api_key","in":"query"},"api-key":{"type":"apiKey","description":"API key authentication via header","name":"X-API-KEY","in":"header"}}},"paths":{"/workflow/execute/external/{workflowId}":{"post":{"tags":["external","Workflows"],"summary":"Execute Workflow (External)","description":"Executes a workflow using external API key authentication","operationId":"executeExternalFlow","parameters":[{"name":"workflowId","in":"path","required":true,"schema":{"type":"string"}},{"name":"X-API-KEY","in":"header","required":false,"schema":{"type":"string"}},{"name":"api_key","in":"query","required":false,"schema":{"type":"string"}},{"name":"configuration_id","in":"query","required":false,"schema":{"type":"string"}},{"name":"response_type","in":"query","required":false,"schema":{"type":"string","default":"json"}},{"name":"start_from_step","in":"query","required":false,"schema":{"type":"integer","format":"int32"}},{"name":"end_at_step","in":"query","required":false,"schema":{"type":"integer","format":"int32"}},{"name":"callback_url","in":"query","required":false,"schema":{"type":"string"}}],"requestBody":{"content":{"*/*":{"schema":{"type":"string"}}}},"responses":{"200":{"description":"Workflow executed successfully","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Execution failed","content":{"*/*":{"schema":{"type":"object"}}}},"401":{"description":"Unauthorized","content":{"*/*":{"schema":{"type":"object"}}}}}}}}}
```


# Scheduled Workflow Execution

Another pattern for executing workflows is scheduling. With Laminar's scheduling option, you can automate your workflows to run at predefined intervals. This is perfect for recurring tasks, regular data synchronization, or automated reporting.

To proceed:

1. Navigate to to the **"Schedule"** tab.
2. Configure the desired schedule using options like daily, hourly, weekly, or a custom cron expression.
3. You can also specify a **default input** that will be used for all scheduled runs.

{% embed url="<https://files.gitbook.com/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeyfSRRmLEurvKZJcO0Tg%2Fuploads%2F2lnDQ3rTn0Jw2NFbdeRj%2FScheduled%20Executions.mp4?alt=media&token=989e3526-4407-471b-bf03-e60c9d564967>" %}


# Getting your API Key

Learn about API Keys in Laminar

## Overview

API keys in Laminar are used to authenticate requests to the Laminar API. They are created and managed in workspace settings and provide secure access to the API.

## Usage

API keys are to be passed in the query parameters of your desired request via the `api_key` param.

```bash
curl -X POST 'https://api.laminar.run/workflow/execute/{workflowId}?api_key=<your_api_key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "data": "your workflow input"
  }'
```

## Best Practices

* Rotate keys periodically for security
* Never share or commit API keys to version control


# Creating a Configuration

Learn how to execute a Create a Configuration with Laminar's API

## Create Configuration Store (External)

> Creates a new configuration store via external API key authentication

```json
{"openapi":"3.0.1","info":{"title":"Laminar API","version":"v1"},"tags":[{"name":"Configuration Stores","description":"Laminar Configuration Store endpoints, for managing configuration stores and properties within workspaces."}],"servers":[{"url":"https://api.laminar.run","description":"Laminar API"}],"security":[{"api-key-query":[]},{"api-key":[]}],"components":{"securitySchemes":{"api-key-query":{"type":"apiKey","description":"API key authentication via query parameter","name":"api_key","in":"query"},"api-key":{"type":"apiKey","description":"API key authentication via header","name":"X-API-KEY","in":"header"}},"schemas":{"CreateConfigurationStoreRequest":{"required":["externalId","name","properties","workspaceId"],"type":"object","properties":{"workspaceId":{"type":"integer","format":"int64"},"name":{"type":"string"},"externalId":{"type":"string"},"properties":{"type":"array","items":{"$ref":"#/components/schemas/PropertyDTO"}}}},"PropertyDTO":{"required":["key","value"],"type":"object","properties":{"key":{"type":"string"},"value":{"type":"string"}}},"ConfigurationStore":{"required":["properties"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"workspaceId":{"type":"integer","format":"int64"},"name":{"type":"string"},"externalId":{"type":"string"},"archivedAt":{"type":"string","format":"date-time"},"properties":{"uniqueItems":true,"type":"array","items":{"$ref":"#/components/schemas/Property"}}}},"Property":{"required":["key","value"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"key":{"type":"string"},"value":{"type":"string"}}}}},"paths":{"/configurations/external":{"post":{"tags":["external","Configuration Stores"],"summary":"Create Configuration Store (External)","description":"Creates a new configuration store via external API key authentication","operationId":"createConfigurationStoreExternal","parameters":[{"name":"X-API-KEY","in":"header","required":false,"schema":{"type":"string"}},{"name":"api_key","in":"query","required":false,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateConfigurationStoreRequest"}}},"required":true},"responses":{"201":{"description":"Configuration store created successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ConfigurationStore"}}}}}}}}}
```


# API Descriptions

Learn about API Descriptions in Laminar

## Overview

Laminar uses API descriptions to make requests to external systems.

Each description specifies:

* The **endpoint URL** to be accessed.
* The **HTTP method** to use for the request (GET, POST, PUT, DELETE).
* The **authentication credentials** required.

## **API Description Model**

| Field           | Type                            |
| --------------- | ------------------------------- |
| id              | Integer (int64)                 |
| url             | String                          |
| methodType      | String (GET, POST, PUT, DELETE) |
| authCredentials | AuthCredentials (Referenced)    |
| name            | String                          |
| description     | String                          |


# Auth Credentials

Learn about Auth Credentials in Laminar

## Overview

Auth Credentials are used by Laminar to make requests on protected API Endpoints described by API Descriptions.

Laminar supports various types of Authentication.

## AuthCredentials Model

| Field    | Type            |
| -------- | --------------- |
| id       | Integer (int64) |
| name     | String          |
| authType | String          |

#### OAuth Client Credentials Grant

| Field        | Type   |
| ------------ | ------ |
| clientId     | String |
| clientSecret | String |
| tokenUrl     | String |
| token        | String |

#### OAuth Password Credentials Grant

| Field        | Type   |
| ------------ | ------ |
| clientId     | String |
| clientSecret | String |
| tokenUrl     | String |
| username     | String |
| password     | String |

#### API Key Authentication

| Field  | Type   |
| ------ | ------ |
| apiKey | String |

#### Bearer Token Authentication

| Field | Type   |
| ----- | ------ |
| token | String |

#### Basic Authentication

| Field    | Type   |
| -------- | ------ |
| username | String |
| password | String |

#### Custom Authentication

| Field               | Type   |
| ------------------- | ------ |
| authorizationHeader | String |
| token               | String |


# Configurations

Learn about Configurations in Laminar

### Overview

Configurations in Laminar manage environment-specific settings, credentials, and properties for workflows. They enable you to run the same workflow with different settings across environments or customers.

A configuration is defined as a set of key-value pairs

<figure><img src="https://2934663110-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeyfSRRmLEurvKZJcO0Tg%2Fuploads%2FCn96PZn5paDt2c9QTLcY%2Fconfigurations.png?alt=media&amp;token=a73d85ed-83fa-4bf4-a71a-24ba60f124bf" alt=""><figcaption><p>Example of a configuration in Laminar</p></figcaption></figure>

### Usage in Flows

You can reference a configuration variable by using the syntax `{{props.<configuration_property_key>}}`

```javascript
(data) => {
  let transformed_data = data.step_1.response
  
  return {
    "lam.httpRequest": {
      "method": "POST",
      "url": "{{props.WAREHOUSE_API_URL}}/warehouse/orders",
      "headers": {
        "Content-Type": "application/json"
      },
      "body": transformed_data
    }
  };
}
```

### Executing with Different Configurations

The same workflow can be executed with different configurations by specifying the `configuration_id`:

```bash
# Execute workflow for Customer A
POST /workflow/execute/{workflowId}?configuration_id=123&api_key=<api_key>
{
  "orderId": "12345",
  "quantity": 2
}

# Execute workflow for Customer B
POST /workflow/execute/{workflowId}?configuration_id=456&api_key=<api_key>
{
  "orderId": "67890",
  "quantity": 1
}
```

The workflow will use the appropriate credentials and properties based on the configuration\_id provided.

### Best Practices

#### Security

* Store sensitive data like credentials in the configuration store
* Rotate credentials regularly
* Use different configurations per environment/customer

#### Organization

* Use meaningful property names
* Keep configurations focused and organized
* Document configuration requirements


# API


# Changelog

Changelog for Laminar's platform

## Laminar Product Changelog

### February 2025

#### Week of February 17

* Introduced V2 Workspaces Page with improved user interface and performance

#### Week of February 12-14

* Enhanced error handling and toast notifications across the platform
* Improved user invitation workflow - users can now accept/decline workspace invitations upon signin/registration
* Fixed issues with CRUD operations for the Issues management feature
* Enhanced error page UX with ability to return to previous page and retry actions
* Bug fixes for change log functionality

#### Week of February 10

* Added HTTP Request keywords for improved API integration
* Enhanced Continuous Deployment (CD) for Production and Staging environments
* Fixed issues with Global Workflow Object

### January 2025

#### Week of January 28

* Enhanced workflow execution panel with improved UX
* Added analytics capabilities for better insight into workflow performance
* Split test and run workflow actions for more granular control

#### Week of January 22-25

* Implemented new sign-up and sign-in flow for improved user onboarding
* Introduced configuration flow for easier system setup
* Added Issues management functionality
* Updated dashboard with composer integration
* Code quality improvements including standardized formatting

#### Week of January 16

* Security updates and dependency fixes

### December 2024

#### Week of December 15

* Fixed delete workflow functionality
* Ensured LAM update operations work correctly
* Improved API descriptions

#### Week of December 3

* Added ability to pass XML request body directly using lam.xmlString
* Introduced lam.pathParams for enhanced routing capabilities
* Added data transformation based on content type for more flexible integrations

### November 2024

#### Week of November 25

* Fixed cache revalidation on update and delete operations
* Improved query parameter handling for API requests

#### Week of November 21

* Enhanced XML compatibility in response handling

#### Week of October 31

* Added XML support for expanded data format capabilities


# Workflow Executions Trace

Laminar provides tools to help you diagnose and fix problems quickly.

Every time a workflow runs, Laminar generates a detailed execution trace. This trace provides a visual representation of the entire lifecycle of that particular workflow run, from start to finish with key information such as:

* overall workflow status
* step run details:
  * run status
  * input
  * output
  * logs and more

{% embed url="<https://files.gitbook.com/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeyfSRRmLEurvKZJcO0Tg%2Fuploads%2FvDHiuSYp7s6vR2PSSifQ%2FWorkflow%20Run%20Walkthrough.mp4?alt=media&token=ca4f7f50-2f53-4904-9750-d5b2571ef000>" %}


# Failure Alert Notifications

By default, when a workflow fails, Laminar automatically sends notifications through two channels:

* In-app notifications that appear in the user's notification centre
* Email notifications sent to the user's registered email address

<figure><img src="https://2934663110-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeyfSRRmLEurvKZJcO0Tg%2Fuploads%2FxrMzRLNInEkLkNyW5tNN%2FScreenshot%202025-07-11%20at%209.20.06%E2%80%AFAM.png?alt=media&amp;token=2615545e-314e-4950-ab91-d750c4eaf8e2" alt=""><figcaption><p>In-App Notifications</p></figcaption></figure>

## Customize Notification Preferences

Users can customize their notification preferences by turning on/off a notification channel by:

1. opening the notification centre
2. clicking on the settings button. A notification preferences menu should appear.
3. toggle on or off the channel you wish to turn on or off.


# Spotlight: Debug Workflows with AI

Laminar's AI Chatbot can analyze your workflow failures and provide actionable insights.

When a workflow fails, you can **attach the failed execution trace** directly to the AI Chatbot conversation. Once attached, you can simply ask the AI to investigate the issue or even suggest fixes for your program code. The AI will analyze the entire trace, including step inputs, outputs, logs, and your program code, to identify potential root causes and offer solutions.

{% embed url="<https://files.gitbook.com/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeyfSRRmLEurvKZJcO0Tg%2Fuploads%2F4Ac9yLZ0mkHipQdqRJZj%2FDebug%20with%20AI.mp4?alt=media&token=2694b465-9a84-473d-baec-25a492cde845>" %}


# Getting your API Key

Learn about API Keys in Laminar

## Overview

API keys in Laminar are used to authenticate requests to the Laminar API. They are created and managed in workspace settings and provide secure access to the API.

## Usage

API keys are to be passed in the query parameters of your desired request via the `api_key` param.

```bash
curl -X POST 'https://api.laminar.run/workflow/execute/{workflowId}?api_key=<your_api_key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "data": "your workflow input"
  }'
```

## Best Practices

* Rotate keys periodically for security
* Never share or commit API keys to version control


# API Descriptions

Learn about API Descriptions in Laminar

## Overview

Laminar uses API descriptions to make requests to external systems.

Each description specifies:

* The **endpoint URL** to be accessed.
* The **HTTP method** to use for the request (GET, POST, PUT, DELETE).
* The **authentication credentials** required.

## **API Description Model**

| Field           | Type                            |
| --------------- | ------------------------------- |
| id              | Integer (int64)                 |
| url             | String                          |
| methodType      | String (GET, POST, PUT, DELETE) |
| authCredentials | AuthCredentials (Referenced)    |
| name            | String                          |
| description     | String                          |


# Auth Credentials

Learn about Auth Credentials in Laminar

## Overview

Auth Credentials are used by Laminar to make requests on protected API Endpoints described by API Descriptions.

Laminar supports various types of Authentication.

## AuthCredentials Model

| Field    | Type            |
| -------- | --------------- |
| id       | Integer (int64) |
| name     | String          |
| authType | String          |

#### OAuth Client Credentials Grant

| Field        | Type   |
| ------------ | ------ |
| clientId     | String |
| clientSecret | String |
| tokenUrl     | String |
| token        | String |

#### OAuth Password Credentials Grant

| Field        | Type   |
| ------------ | ------ |
| clientId     | String |
| clientSecret | String |
| tokenUrl     | String |
| username     | String |
| password     | String |

#### API Key Authentication

| Field  | Type   |
| ------ | ------ |
| apiKey | String |

#### Bearer Token Authentication

| Field | Type   |
| ----- | ------ |
| token | String |

#### Basic Authentication

| Field    | Type   |
| -------- | ------ |
| username | String |
| password | String |

#### Custom Authentication

| Field               | Type   |
| ------------------- | ------ |
| authorizationHeader | String |
| token               | String |


# API


# Storing Credentials/Variables

Learn about how to store Credentials/Variables in Laminar with Configurations

Configurations in Laminar are key-value stores that manage environment-specific settings, credentials, and properties for workflows. They enable you to run the same workflow with different settings across environments or customers.

## Usage

To understand how configurations work, we'll use the completed example from [Getting Started](/getting-started).

Suppose we had different API base urls for each environment: `STAGING` and `PROD`. To be able to reuse the workflow with different variables, we would need to create two new configurations. To do so:

1. from the workflow editor, click on the **Select a configuration** dropdown
2. click on the New Configuration from the menu. It should bring up a configuration form
3. Fill out configuration form. Enter name, id and set of properties
4. \[OPTIONAL] Repeat steps 1 through 3 for the second configuration
5. Update step programs. Substitute the hardcoded value to abstract with the syntax `{{config.<configuration_property_key>}}` and save your changes.

**Example:**

```diff
(data) => {
    // The body of the request is the output from the previous step (step_1)
    const requestBody = data.step_1.data;

    return {
        "lam.httpRequest": {
            "method": "POST",
-           "url": "https://shipping-logistics-loic21.replit.app/api/orders",
+           "url": "{{config.shipping_api_base_url}}",
            "headers": {
                "Content-Type": "application/json"
            },
            "body": requestBody
        }
    };
}
```

{% embed url="<https://files.gitbook.com/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeyfSRRmLEurvKZJcO0Tg%2Fuploads%2FM3X30nrHPRqJs95piX1g%2FCreating%20CONFIG.mp4?alt=media&token=06e80604-d1f8-4d49-b0a9-5f11f140a779>" %}

To run the workflow with a specific configuration, make sure you select the right configuration from the editor before:

1. clicking on the **Run Workflow** button if triggering the workflow in-app
2. copying the workflow execution link if triggering workflow remotely

## **Dynamic configuration updates**

Dynamic configuration updates allow workflows to adapt to changing data and scenarios on the fly. With the `lam.updateConfig` operation, you can seamlessly modify configuration settings directly from a flow. \
This flexibility is crucial for maintaining an agile workflow environment. \
For example, you can update a configuration with the latest processing timestamp or dynamically adjust thresholds, ensuring your workflow remains responsive and efficient. By setting `createIfNotExists` to true, you can also ensure new configurations are automatically created if they don't already exist, further enhancing your system's adaptability.<br>

```javascript
(data) => {
  const { input } = data;

  return {
    "lam.updateConfig": {
        "configurationId": "my-config",
        "properties": [
          {
            "key": "lastProcessedTimestamp",
            "value": new Date().toISOString()
          },
          {
            "key": "recordCount",
            "value": "1500"
          }
        ],
        "createIfNotExists": true,
        "configurationName": "Auto-generated Config"
      }
    }
};
```


# Batching HTTP Requests

Learn how to make multiple HTTP requests concurrently in a Laminar Step

When you need to make multiple HTTP requests concurrently within a single step, `lam.httpRequests` is your go-to. This is more efficient than creating multiple sequential `lam.httpRequest` steps if the requests don't depend on each other's immediate output.

The value associated with `lam.httpRequests` is an array of objects, where each object has the same structure as a single `lam.httpRequest` definition.

The output of a `lam.httpRequests` step will be an array of response objects, corresponding to the order of requests defined.

**Example:** Suppose we want to handle incoming orders and we want to send them to different fulfillment centres.

{% stepper %}
{% step %}

### Send Orders to Warehouse

```javascript
(data) => {
  const { input } = data;
  
  return {
    "lam.httpRequests": [
      {
        "method": "POST",
        "url": "{{config.baseUrl}}/send-to-warehouse/premium",
        "body": input.premiumOrder
      },
      {
        "method": "POST",
        "url": "{{config.baseUrl}}/send-to-warehouse/regular",
        "pathParams": {
          "itemId": input.regularOrder
        }
      }
    ]
  };
}
```

{% endstep %}

{% step %}

### Update Status Orders

```javascript
(data) => {
  const { input } = data;
  const { response } = data;
  
  return {
    "lam.httpRequests": [
      {
        "method": "PUT",
        "url": `{{config.baseUrl}}/order/${input.premiumOrder.id}`,
        "body": {
          "status": "Processing",
          "tracking_url": response[0].tracking_url,
          "shipment_reference_id": response[0].tracking_number,
        }
      },
      {
        "method": "PUT",
        "url": `{{config.baseUrl}}/order/${input.regularOrder.id}`,
        "body": {
          "status": "Processing",
          "tracking_url": response[1].tracking_url,
          "shipment_reference_id": response[1].tracking_number,
        }
      }
    ]
  };
}
```

{% endstep %}
{% endstepper %}


# Retrying HTTP Requests

When working with external APIs, network failures and temporary service outages are inevitable. The `retry` option in `lam.httpRequest` and `lam.httpRequests` allows you to automatically retry failed requests.

The retry functionality is **optional;** just add a `retry` object with `maxAttempts` to your HTTP request.

**Important:** Retries are not enabled by default. You must explicitly specify retry behaviour when creating your workflow steps.<br>

**`retry` Properties**

<table><thead><tr><th width="200.11328125">Property</th><th width="148.4609375">Type</th><th width="206.83203125">Default</th><th>Description</th></tr></thead><tbody><tr><td>maxAttempts</td><td>int</td><td>required</td><td>Total number of attempts (1 = no<br>retry)</td></tr><tr><td>strategy</td><td>string</td><td>exponential</td><td>"exponential" or "fixed"</td></tr><tr><td>initialDelay</td><td>string/number</td><td>1s</td><td>First retry delay</td></tr><tr><td>maxDelay</td><td>string/number</td><td>30s</td><td>Maximum delay cap</td></tr><tr><td>multiplier</td><td>double</td><td>2.0</td><td>Exponential backoff multiplier</td></tr><tr><td>retryableStatusCodes</td><td>array[int]</td><td>[429,500,502,503,504]</td><td>HTTP status codes to retry</td></tr><tr><td>retryableExceptions</td><td>array[string]</td><td>["timeout","connection"]</td><td>Exception types to retry</td></tr></tbody></table>

**Example:** Suppose we're processing orders and need to reliably send them to external fulfillment services that occasionally experience temporary outages.

{% stepper %}
{% step %}

#### Send Order to Primary Warehouse (With Retry)

```javascript
(data) => {
  const { input } = data;

  return {
    "lam.httpRequest": {
      method: "POST",
      url: "{{config.primaryWarehouseUrl}}/api/orders",
      headers: {
        Authorization: "Bearer {{config.warehouseApiKey}}",
        "Content-Type": "application/json",
      },
      body: {
        orderId: input.orderId,
        items: input.items,
        priority: "high",
        shippingAddress: input.shippingAddress,
      },
      // added here
      retry: {
        maxAttempts: 3,
      },
    },
  };
};
```

{% endstep %}

{% step %}

#### Update Multiple Services (Batch Retry)

```javascript
(data) => {
  const { input } = data;
  const fulfillmentResponse = data.step_2.response || data.step_1.response;

  return {
    "lam.httpRequests": [
      {
        method: "PUT",
        url: `{{config.orderServiceUrl}}/orders/${input.orderId}/status`,
        headers: {
          Authorization: "Bearer {{config.orderServiceKey}}",
        },
        body: {
          status: "processing",
          trackingNumber: fulfillmentResponse.trackingNumber,
          warehouse: fulfillmentResponse.warehouseId,
        },
        // added here
        retry: {
          maxAttempts: 2,
        },
      },
      {
        method: "POST",
        url: "{{config.notificationServiceUrl}}/send-confirmation",
        headers: {
          Authorization: "Bearer {{config.notificationKey}}",
        },
        body: {
          customerId: input.customerId,
          orderId: input.orderId,
          trackingNumber: fulfillmentResponse.trackingNumber,
          type: "order_confirmed",
        },
        // added here
        retry: {
          maxAttempts: 4,
        },
      },
    ],
  };
};
```

{% endstep %}
{% endstepper %}


# Polling

When working with asynchronous operations, many APIs return immediately with a job ID or task reference, requiring you to poll for completion status. The `loopUntil` option in `lam.httpRequest` and `lam.httpRequests` allows you to automatically poll an endpoint until a specific condition is met.

The polling functionality is optional; just add a `loopUntil` object with a condition function to your HTTP request.

**Important:** Polling is not enabled by default. You must explicitly specify polling behavior when creating your workflow steps.

### `loopUntil` Properties

| Property       | Type                         | Required | Default | Description                                                                        |
| -------------- | ---------------------------- | -------- | ------- | ---------------------------------------------------------------------------------- |
| `condition`    | String (JavaScript function) | Yes      | -       | Function that receives `ctx` object and returns boolean. When true, polling stops. |
| `maxAttempts`  | Number                       | No       | 10      | Maximum number of polling attempts before giving up                                |
| `strategy`     | String                       | No       | 'fixed' | Delay strategy: 'fixed' or 'exponential'                                           |
| `initialDelay` | String                       | No       | '1s'    | Initial delay between polls (e.g., '1s', '500ms', '2m')                            |
| `maxDelay`     | String                       | No       | '30s'   | Maximum delay between polls for exponential strategy                               |
| `multiplier`   | Number                       | No       | 2       | Multiplier for exponential backoff strategy                                        |

The condition function receives a `ctx` object with:

* `ctx.response` - The parsed response body
* `ctx.status` - HTTP status code
* `ctx.headers` - Response headers

**Example:** Suppose we're starting a data processing job that takes time to complete, and we need to wait for it to finish before proceeding.

**Start Data Processing Job**

```javascript
(data) => {
  const { input } = data;

  return {
    "lam.httpRequest": {
      method: "POST",
      url: "{{config.dataProcessorUrl}}/api/jobs",
      headers: {
        Authorization: "Bearer {{config.processorApiKey}}",
        "Content-Type": "application/json",
      },
      body: {
        datasetId: input.datasetId,
        operation: "transform",
        outputFormat: "parquet",
      },
      // Poll until job completes
      loopUntil: {
        condition: "(ctx) => ctx.response.status === 'completed'",
        maxAttempts: 20,
        strategy: "exponential",
        initialDelay: "2s",
        maxDelay: "60s",
      },
    },
  };
};
```


# Executing Shell Commands

lam.shell is a secure shell execution system that allows you to safely execute bash shell commands while providing the ability to chain together any type of business logic. This system enables you to build complex workflows by combining shell commands with control flow mechanisms and configuration management.

{% stepper %}
{% step %}
**Basic `lam.shell` Step Structure**

```javascript
(data) => {
    const script = `#!/bin/bash
    set -e
    
    echo "Hello from lam.shell"
    ls -la /data/
    `;
    
    return {
        "lam.shell": {
            "script": script,
            "timeout": 300
        }
    };
}
```

{% endstep %}

{% step %}
**File Processing Step with Binary Data**

```javascript
(data) => {
    const fileNames = data.input.files || [];
    
    const script = `#!/bin/bash
    set -e
    
    echo "Processing ${fileNames.length} zip files..."
    
    # List available files
    ls -al /data/input/
    
    # Process each zip file
    for file in /data/input/*.zip; do
        if [ -f "$file" ]; then
            filename=$(basename "$file" .zip)
            echo "Extracting $filename..."
            
            # Create extraction directory
            mkdir -p "/data/output/$filename"
            
            # Extract zip file
            unzip "$file" -d "/data/output/$filename"
            
            # List extracted contents
            echo "Contents of $filename:"
            ls -la "/data/output/$filename"
        fi
    done
    
    echo "File processing completed"
    `;
    
    return {
        "lam.shell": {
            "script": script,
            "binaryDataIds": data.step_6.response, // Pass binary data IDs
            "environment": {
                "SFDX_DISABLE_AUTOUPDATE": "true",
                "SFDX_USE_GENERIC_UNIX_KEYCHAIN": "true"
            },
            "timeout": 600
        }
    };
}
```

{% endstep %}

{% step %}
**Configuration-Driven Step**

```javascript
(data) => {
    const script = `#!/bin/bash
    set -e
    
    # Use configuration values
    API_URL="{{config.apiUrl}}"
    API_KEY="{{config.apiKey}}"
    BATCH_SIZE="{{config.batchSize}}"
    
    echo "Starting data processing with batch size: $BATCH_SIZE"
    
    # Validate configuration
    if [ -z "$API_URL" ]; then
        echo "Error: API URL not configured"
        exit 1
    fi
    
    # Process data in batches
    TOTAL_RECORDS=$(wc -l < /data/input/data.csv)
    BATCHES=$(( (TOTAL_RECORDS + BATCH_SIZE - 1) / BATCH_SIZE ))
    
    echo "Processing $TOTAL_RECORDS records in $BATCHES batches"
    
    for ((i=1; i<=BATCHES; i++)); do
        START_LINE=$(( (i-1) * BATCH_SIZE + 1 ))
        END_LINE=$(( i * BATCH_SIZE ))
        
        echo "Processing batch $i (lines $START_LINE-$END_LINE)"
        
        # Extract batch
        sed -n "${START_LINE},${END_LINE}p" /data/input/data.csv > /tmp/batch_$i.csv
        
        # Send batch to API
        curl -X POST "$API_URL/batch" \
             -H "Authorization: Bearer $API_KEY" \
             -H "Content-Type: text/csv" \
             --data-binary @/tmp/batch_$i.csv \
             -o /data/output/batch_${i}_response.json
        
        # Clean up temp file
        rm /tmp/batch_$i.csv
    done
    
    echo "Batch processing completed"
    `;
    
    return {
        "lam.shell": {
            "script": script,
            "environment": {
                "CURL_CA_BUNDLE": "/etc/ssl/certs/ca-certificates.crt"
            },
            "timeout": 1800
        }
    };
}
```

{% endstep %}
{% endstepper %}


# Early Exit

When writing a workflow step program and you need to exit the workflow early because a pre-condition is not met, use the keyword `lam.exit` in your return object.

**Example:** Using our order fulfillment workflow example from [Getting Started](/getting-started), suppose we needed to check that incoming order is not for a digital service before proceeding to send data to fulfillment centre.

{% stepper %}
{% step %}

### Send Orders to Fulfillment Centre

```diff
(data) => {
    const inputOrder = data.input;
    
+   if (inputOrder.isDigital) {
+       return {
+         "lam.exit": true,
+         // payload to be returned
+         "data": { 
+           "message": "Skipped. Digital service orders should not be sent to fulfillment centre.",
+           "order": inputOrder,
+         }
+       };
+   }

    // Use Array.map to transform the items array
    const transformedItems = inputOrder.items.map(item => ({
        sku: item.product.sku,
        name: item.product.name,
        quantity: item.quantity,
        price: parseFloat(item.price) // Convert price from string to number
    }));

    // Construct the final output object
    const outputOrder = {
        customer_name: inputOrder.customerName,
        customer_email: inputOrder.customerEmail,
        customer_phone: "+1234567890", // Placeholder as source data is missing
        shipping_address: {
            street: `${inputOrder.shippingAddressLine1 || ''} ${inputOrder.shippingAddressLine2 || ''}`.trim(),
            city: inputOrder.shippingCity,
            state: inputOrder.shippingState,
            zip: inputOrder.shippingZipCode,
            country: inputOrder.shippingCountry
        },
        items: transformedItems,
        shipping_method: "standard" // Default value as source data is missing
    };

    return outputOrder;
}
```

{% endstep %}

{% step %}

### ...

{% endstep %}
{% endstepper %}


# Uploading and Downloading Files

Laminar provides mechanisms for handling binary data (files) within your workflows, particularly in conjunction with HTTP requests and shell scripts.

When an `http` request (using `lam.httpRequest`) results in a file download, Laminar automatically stores the file and provides a `binaryDataId` in the step's response.

{% code title="Sample response for downloading file" %}

```json
{
  "lam.httpStatusCode": 200,
  "lam.httpStatusText": "OK",
  "lam.success": true,
  "lam.binaryDataIndicator": true,
  "lam.contentType": "application/zip",
  "lam.fileName": "fileName",
  "lam.contentLength": 625,
  "lam.binaryDataId": "binaryId"
}

```

{% endcode %}

You can access this ID via `data.step_N.response['lam.binaryDataId']`.

## **Processing files in script:**

To process a downloaded file (or any binary data referenced by a `binaryDataId`) within a `lam.shell` script, you must include the `binaryDataIds` array in your `lam.shell` definition.

Laminar will make these files available to your shell script in its execution environment.

**Example Workflow:**

{% stepper %}
{% step %}

### Download Report

```javascript
(data) => {
    // This step simulates downloading a CSV report.
    return {
        "lam.httpRequest": {
            "method": "GET",
            "url": "https://mockserver.com/download-report.csv"
        }
    };
}
```

After this step executes, `data.step_1.response['lam.binaryDataId']` will hold the reference to the downloaded CSV file.
{% endstep %}

{% step %}

### Print Report

```javascript
(data) => {
    // Get the binaryDataId from the previous step's response (Step 1).
    const downloadedFileId = data.step_1.response['lam.binaryDataId'];
    const fileName = data.step_1.response['lam.fileName'];

    return {
        "lam.shell": {
            // The script will operate on the downloaded file.
            // Laminar makes files referenced by binaryDataIds available by their original names
            // or a generic 'input' if no name is available.
            "script": `
                echo "Processing downloaded file..."
                # Assuming the downloaded file is named 'report.csv' or similar, or just 'input'
                # You might need to inspect the actual filename provided by Lunar in logs.
                cat ${fileName} | wc -l
            `,
            "binaryDataIds": [downloadedFileId],
        }
    };
}
```

{% endstep %}
{% endstepper %}


# Custom Response Formatting

By default, Laminar workflows return the output of the last step with a 200 status code (if successful) or 500 (if failed). The `lam.response` feature gives you more flexibility to return custom HTTP responses with specific status codes and formatting.

This is useful when building API endpoints that need specific HTTP behaviors.

### How It Works

When any step returns an object containing `lam.response`, Laminar will:

1. **Exit immediately** - Stop executing the workflow at that step
2. **Skip remaining steps** - No subsequent steps will be executed
3. **Return custom response** - Use your specified format and status code

### Response Structure

* **statusCode** (required): HTTP status code (200, 201, 400, 404, 500, etc.)
* **message** (required): Human-readable message
* **data** (optional): Response data
* **error** (optional): Error information for error responses

{% code title="Schema" %}

```json
{
  "lam.response": {
    "statusCode": 200 | 201 | 400 | 404 | ...,
    "message": "message",
    "data": {
      // Your response data here
    },
    "error": {
      "code": "ERROR_CODE",
      "message": "Error description",
      "details": "Additional error details",
      "field_errors": {
        "field1": "Field-specific error message"
      }
    }
  }
}
```

{% endcode %}

### Examples

#### Success Response

```javascript
(data) => {
  return {
    "lam.response": {
      statusCode: 201,
      message: "Dispatch note created successfully",
      data: data.step_4.response,
    },
  };
};
```

#### Error Response

```javascript
(data) => {
  if (!data.input.email) {
    return {
      "lam.response": {
        statusCode: 400,
        message: "Email is required",
        error: {
          code: "VALIDATION_ERROR",
          field: "email",
        },
      },
    };
  }

  // Continue with normal processing
  return { validated: true };
};
```

#### Authentication Check

```javascript
(data) => {
  if (!data.input.apiKey || data.input.apiKey !== "{{config.validApiKey}}") {
    return {
      "lam.response": {
        statusCode: 401,
        message: "Invalid API key",
        error: {
          code: "UNAUTHORIZED",
        },
      },
    };
  }

  // Continue with normal workflow
  return { authorized: true };
};
```

### Use Cases

* **API endpoints** that need specific HTTP status codes
* **Early validation** to exit workflows when conditions aren't met
* **Authentication checks** with proper 401/403 responses
* **Error handling** with custom error formats


# Cron Job Management (lam.cron)

lam.cron is a powerful scheduling system that allows you to create, manage, and execute scheduled tasks (cron jobs) within your workflows. Users create cron steps by defining step programs that return `lam.cron` objects containing scheduling information, with support for creating, updating, deleting, and listing scheduled tasks.

## Key Concepts

A Cron job is created by defining step programs that return `lam.cron` object. Each cron operation consists of:

* **Operation Types**: Create, update, or delete
* **Cron Schedules**: Standard cron syntax for defining when tasks run
* **URL**: Destination of the POST request and body
* **Body**: JSON body that will be sent as part of the cron job

## Creating a cron job

```javascript
(data) => {
    return {
        "lam.cron": {
            "operation": "create",
            "name": "Daily Report",
            "schedule": "0 0 9 * * *",
            "url": "https://api.laminar.run/workflow/execute/external/308?api_key=your-key&configuration_id=58",
            "body": {
                "report_type": "daily"
            }
        }
    };
}
```

<p align="right"><strong>Response to creating the cron job</strong></p>

## Updating a cron job

```javascript
(data) => {
    return {
        "lam.cron": {
            "operation": "update",
            "name": "Daily Report",
            "schedule": "0 0 10 * * *", // Change from 9 AM to 10 AM
            "url": "https://api.laminar.run/workflow/execute/external/308?api_key=your-key&configuration_id=58",
            "body": {
                "report_type": "daily"
            }
        }
    };
}
```

## Deleting a cron job

```javascript
(data) => {
  // Your code here
  return   {
    "lam.cron": {
      "operation": "delete",
      "jobId":"229988fd-0308-472a-8385-380f2b97f461"
    }
  };
}
```


# Execute a Workflow

## Execute Workflow (External)

> Executes a workflow using external API key authentication

```json
{"openapi":"3.0.1","info":{"title":"Laminar API","version":"v1"},"tags":[{"name":"Workflows","description":"Laminar Workflow endpoints, for managing workflows and workflow executions."}],"servers":[{"url":"https://api.laminar.run","description":"Laminar API"}],"security":[{"api-key-query":[]},{"api-key":[]}],"components":{"securitySchemes":{"api-key-query":{"type":"apiKey","description":"API key authentication via query parameter","name":"api_key","in":"query"},"api-key":{"type":"apiKey","description":"API key authentication via header","name":"X-API-KEY","in":"header"}}},"paths":{"/workflow/execute/external/{workflowId}":{"post":{"tags":["external","Workflows"],"summary":"Execute Workflow (External)","description":"Executes a workflow using external API key authentication","operationId":"executeExternalFlow","parameters":[{"name":"workflowId","in":"path","required":true,"schema":{"type":"string"}},{"name":"X-API-KEY","in":"header","required":false,"schema":{"type":"string"}},{"name":"api_key","in":"query","required":false,"schema":{"type":"string"}},{"name":"configuration_id","in":"query","required":false,"schema":{"type":"string"}},{"name":"response_type","in":"query","required":false,"schema":{"type":"string","default":"json"}},{"name":"start_from_step","in":"query","required":false,"schema":{"type":"integer","format":"int32"}},{"name":"end_at_step","in":"query","required":false,"schema":{"type":"integer","format":"int32"}},{"name":"callback_url","in":"query","required":false,"schema":{"type":"string"}}],"requestBody":{"content":{"*/*":{"schema":{"type":"string"}}}},"responses":{"200":{"description":"Workflow executed successfully","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Execution failed","content":{"*/*":{"schema":{"type":"object"}}}},"401":{"description":"Unauthorized","content":{"*/*":{"schema":{"type":"object"}}}}}}}}}
```


# Managing Configurations

## Remove Property (External)

> Removes a property from the configuration store via external API key authentication

```json
{"openapi":"3.0.1","info":{"title":"Laminar API","version":"v1"},"tags":[{"name":"Configuration Stores","description":"Laminar Configuration Store endpoints, for managing configuration stores and properties within workspaces."}],"servers":[{"url":"https://api.laminar.run","description":"Laminar API"}],"security":[{"api-key-query":[]},{"api-key":[]}],"components":{"securitySchemes":{"api-key-query":{"type":"apiKey","description":"API key authentication via query parameter","name":"api_key","in":"query"},"api-key":{"type":"apiKey","description":"API key authentication via header","name":"X-API-KEY","in":"header"}}},"paths":{"/configurations/{externalId}/properties/{key}/external":{"delete":{"tags":["external","Configuration Stores"],"summary":"Remove Property (External)","description":"Removes a property from the configuration store via external API key authentication","operationId":"removePropertyExternal","parameters":[{"name":"X-API-KEY","in":"header","required":false,"schema":{"type":"string"}},{"name":"api_key","in":"query","required":false,"schema":{"type":"string"}},{"name":"externalId","in":"path","required":true,"schema":{"type":"string"}},{"name":"key","in":"path","required":true,"schema":{"type":"string"}},{"name":"workspaceId","in":"query","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"200":{"description":"Property removed successfully"},"401":{"description":"Invalid API key"},"404":{"description":"Configuration store or property not found"}}}}}}
```

## Bulk Update Properties (External)

> Updates multiple properties via external API key authentication

```json
{"openapi":"3.0.1","info":{"title":"Laminar API","version":"v1"},"tags":[{"name":"Configuration Stores","description":"Laminar Configuration Store endpoints, for managing configuration stores and properties within workspaces."}],"servers":[{"url":"https://api.laminar.run","description":"Laminar API"}],"security":[{"api-key-query":[]},{"api-key":[]}],"components":{"securitySchemes":{"api-key-query":{"type":"apiKey","description":"API key authentication via query parameter","name":"api_key","in":"query"},"api-key":{"type":"apiKey","description":"API key authentication via header","name":"X-API-KEY","in":"header"}},"schemas":{"BulkUpdatePropertiesRequest":{"required":["properties"],"type":"object","properties":{"properties":{"type":"array","items":{"$ref":"#/components/schemas/PropertyDTO"}}}},"PropertyDTO":{"required":["key","value"],"type":"object","properties":{"key":{"type":"string"},"value":{"type":"string"}}},"ConfigurationStore":{"required":["properties"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"workspaceId":{"type":"integer","format":"int64"},"name":{"type":"string"},"externalId":{"type":"string"},"archivedAt":{"type":"string","format":"date-time"},"properties":{"uniqueItems":true,"type":"array","items":{"$ref":"#/components/schemas/Property"}}}},"Property":{"required":["key","value"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"key":{"type":"string"},"value":{"type":"string"}}}}},"paths":{"/configurations/{externalId}/properties/external":{"put":{"tags":["external","Configuration Stores"],"summary":"Bulk Update Properties (External)","description":"Updates multiple properties via external API key authentication","operationId":"bulkUpdatePropertiesExternal","parameters":[{"name":"X-API-KEY","in":"header","required":false,"schema":{"type":"string"}},{"name":"api_key","in":"query","required":false,"schema":{"type":"string"}},{"name":"externalId","in":"path","required":true,"schema":{"type":"string"}},{"name":"workspaceId","in":"query","required":true,"schema":{"type":"integer","format":"int64"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkUpdatePropertiesRequest"}}},"required":true},"responses":{"200":{"description":"Properties updated successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ConfigurationStore"}}}},"401":{"description":"Invalid API key"},"404":{"description":"Configuration store not found"}}}}}}
```

## Get Configuration Keys (External)

> Retrieves list of property keys via external API key authentication

```json
{"openapi":"3.0.1","info":{"title":"Laminar API","version":"v1"},"tags":[{"name":"Configuration Stores","description":"Laminar Configuration Store endpoints, for managing configuration stores and properties within workspaces."}],"servers":[{"url":"https://api.laminar.run","description":"Laminar API"}],"security":[{"api-key-query":[]},{"api-key":[]}],"components":{"securitySchemes":{"api-key-query":{"type":"apiKey","description":"API key authentication via query parameter","name":"api_key","in":"query"},"api-key":{"type":"apiKey","description":"API key authentication via header","name":"X-API-KEY","in":"header"}},"schemas":{"ConfigurationKeysResponse":{"required":["externalId","keys","name"],"type":"object","properties":{"keys":{"type":"array","items":{"type":"string"}},"externalId":{"type":"string"},"name":{"type":"string"}}}}},"paths":{"/configurations/{externalId}/keys/external":{"get":{"tags":["external","Configuration Stores"],"summary":"Get Configuration Keys (External)","description":"Retrieves list of property keys via external API key authentication","operationId":"getConfigurationKeysExternal","parameters":[{"name":"X-API-KEY","in":"header","required":false,"schema":{"type":"string"}},{"name":"api_key","in":"query","required":false,"schema":{"type":"string"}},{"name":"externalId","in":"path","required":true,"schema":{"type":"string"}},{"name":"workspaceId","in":"query","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"200":{"description":"Configuration keys retrieved successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ConfigurationKeysResponse"}}}},"401":{"description":"Invalid API key","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ConfigurationKeysResponse"}}}},"404":{"description":"Configuration store not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ConfigurationKeysResponse"}}}}}}}}}
```

## Create Configuration Store (External)

> Creates a new configuration store via external API key authentication

```json
{"openapi":"3.0.1","info":{"title":"Laminar API","version":"v1"},"tags":[{"name":"Configuration Stores","description":"Laminar Configuration Store endpoints, for managing configuration stores and properties within workspaces."}],"servers":[{"url":"https://api.laminar.run","description":"Laminar API"}],"security":[{"api-key-query":[]},{"api-key":[]}],"components":{"securitySchemes":{"api-key-query":{"type":"apiKey","description":"API key authentication via query parameter","name":"api_key","in":"query"},"api-key":{"type":"apiKey","description":"API key authentication via header","name":"X-API-KEY","in":"header"}},"schemas":{"CreateConfigurationStoreRequest":{"required":["externalId","name","properties","workspaceId"],"type":"object","properties":{"workspaceId":{"type":"integer","format":"int64"},"name":{"type":"string"},"externalId":{"type":"string"},"properties":{"type":"array","items":{"$ref":"#/components/schemas/PropertyDTO"}}}},"PropertyDTO":{"required":["key","value"],"type":"object","properties":{"key":{"type":"string"},"value":{"type":"string"}}},"ConfigurationStore":{"required":["properties"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"workspaceId":{"type":"integer","format":"int64"},"name":{"type":"string"},"externalId":{"type":"string"},"archivedAt":{"type":"string","format":"date-time"},"properties":{"uniqueItems":true,"type":"array","items":{"$ref":"#/components/schemas/Property"}}}},"Property":{"required":["key","value"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"key":{"type":"string"},"value":{"type":"string"}}}}},"paths":{"/configurations/external":{"post":{"tags":["external","Configuration Stores"],"summary":"Create Configuration Store (External)","description":"Creates a new configuration store via external API key authentication","operationId":"createConfigurationStoreExternal","parameters":[{"name":"X-API-KEY","in":"header","required":false,"schema":{"type":"string"}},{"name":"api_key","in":"query","required":false,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateConfigurationStoreRequest"}}},"required":true},"responses":{"201":{"description":"Configuration store created successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ConfigurationStore"}}}}}}}}}
```

## Delete Configuration Store (External)

> Archives a configuration store via external API key authentication

```json
{"openapi":"3.0.1","info":{"title":"Laminar API","version":"v1"},"tags":[{"name":"Configuration Stores","description":"Laminar Configuration Store endpoints, for managing configuration stores and properties within workspaces."}],"servers":[{"url":"https://api.laminar.run","description":"Laminar API"}],"security":[{"api-key-query":[]},{"api-key":[]}],"components":{"securitySchemes":{"api-key-query":{"type":"apiKey","description":"API key authentication via query parameter","name":"api_key","in":"query"},"api-key":{"type":"apiKey","description":"API key authentication via header","name":"X-API-KEY","in":"header"}}},"paths":{"/configurations/{externalId}/external":{"delete":{"tags":["external","Configuration Stores"],"summary":"Delete Configuration Store (External)","description":"Archives a configuration store via external API key authentication","operationId":"deleteConfigurationStoreExternal","parameters":[{"name":"X-API-KEY","in":"header","required":false,"schema":{"type":"string"}},{"name":"api_key","in":"query","required":false,"schema":{"type":"string"}},{"name":"externalId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Configuration store archived successfully"},"401":{"description":"Invalid API key"},"404":{"description":"Configuration store not found"}}}}}}
```

## Restore Configuration Store (External)

> Restores an archived configuration store via external API key authentication

```json
{"openapi":"3.0.1","info":{"title":"Laminar API","version":"v1"},"tags":[{"name":"Configuration Stores","description":"Laminar Configuration Store endpoints, for managing configuration stores and properties within workspaces."}],"servers":[{"url":"https://api.laminar.run","description":"Laminar API"}],"security":[{"api-key-query":[]},{"api-key":[]}],"components":{"securitySchemes":{"api-key-query":{"type":"apiKey","description":"API key authentication via query parameter","name":"api_key","in":"query"},"api-key":{"type":"apiKey","description":"API key authentication via header","name":"X-API-KEY","in":"header"}},"schemas":{"ConfigurationStore":{"required":["properties"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"workspaceId":{"type":"integer","format":"int64"},"name":{"type":"string"},"externalId":{"type":"string"},"archivedAt":{"type":"string","format":"date-time"},"properties":{"uniqueItems":true,"type":"array","items":{"$ref":"#/components/schemas/Property"}}}},"Property":{"required":["key","value"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"key":{"type":"string"},"value":{"type":"string"}}}}},"paths":{"/configurations/{externalId}/restore/external":{"put":{"tags":["external","Configuration Stores"],"summary":"Restore Configuration Store (External)","description":"Restores an archived configuration store via external API key authentication","operationId":"restoreConfigurationStoreExternal","parameters":[{"name":"X-API-KEY","in":"header","required":false,"schema":{"type":"string"}},{"name":"api_key","in":"query","required":false,"schema":{"type":"string"}},{"name":"externalId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Configuration store restored successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ConfigurationStore"}}}},"401":{"description":"Invalid API key","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ConfigurationStore"}}}},"404":{"description":"Archived configuration store not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ConfigurationStore"}}}}}}}}}
```


# Changelog

Changelog for Laminar's platform

## Laminar Product Changelog

### February 2025

#### Week of February 17

* Introduced V2 Workspaces Page with improved user interface and performance

#### Week of February 12-14

* Enhanced error handling and toast notifications across the platform
* Improved user invitation workflow - users can now accept/decline workspace invitations upon signin/registration
* Fixed issues with CRUD operations for the Issues management feature
* Enhanced error page UX with ability to return to previous page and retry actions
* Bug fixes for change log functionality

#### Week of February 10

* Added HTTP Request keywords for improved API integration
* Enhanced Continuous Deployment (CD) for Production and Staging environments
* Fixed issues with Global Workflow Object

### January 2025

#### Week of January 28

* Enhanced workflow execution panel with improved UX
* Added analytics capabilities for better insight into workflow performance
* Split test and run workflow actions for more granular control

#### Week of January 22-25

* Implemented new sign-up and sign-in flow for improved user onboarding
* Introduced configuration flow for easier system setup
* Added Issues management functionality
* Updated dashboard with composer integration
* Code quality improvements including standardized formatting

#### Week of January 16

* Security updates and dependency fixes

### December 2024

#### Week of December 15

* Fixed delete workflow functionality
* Ensured LAM update operations work correctly
* Improved API descriptions

#### Week of December 3

* Added ability to pass XML request body directly using lam.xmlString
* Introduced lam.pathParams for enhanced routing capabilities
* Added data transformation based on content type for more flexible integrations

### November 2024

#### Week of November 25

* Fixed cache revalidation on update and delete operations
* Improved query parameter handling for API requests

#### Week of November 21

* Enhanced XML compatibility in response handling

#### Week of October 31

* Added XML support for expanded data format capabilities


# Product Support

## Service Status Page

Stay informed about the real-time status of Laminar services, scheduled maintenances, and any ongoing incidents by checking out <https://status.laminar.run/>.

## Contact Support

For technical issues, bug reports, or personalized assistance, we're always happy to help. Just shoot us an email at  `support@laminar.com`&#x20;


