> For the complete documentation index, see [llms.txt](https://docs.cybus.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.cybus.io/2-4-2/guides/system-connectivity/mes-erp-business-systems/tulip-integration.md).

# Tulip Integration

This guide describes how to integrate the Tulip frontline operations platform with Connectware. You configure a service commissioning file that streams shop floor data into Tulip machine attributes and Tulip Tables through the Tulip API, and polls a Tulip Table back into the Connectware MQTT broker. A complete example file is available at the end of this guide.

## Objectives

* Establishing a connection between Connectware and the Tulip API using an API token.
* Streaming machine values, such as temperature, into Tulip machine attributes.
* Creating Tulip Table records from shop floor events.
* Polling a Tulip Table and publishing the records to the Connectware MQTT broker.

## Prerequisites

To follow this guide, you will need the following:

* A running instance of Cybus Connectware.
* Access to a Tulip instance (`https://<instance>.tulip.co`) with permission to create API tokens.
* A Tulip API token with the `attributes:write`, `tables:read`, and `tables:write` scopes. Copy the API key and secret when you create the token. For more information, see [Set up a Tulip API Token](https://support.tulip.co/docs/set-up-a-tulip-api) in the Tulip Knowledge Base.
* For machine attributes: a machine with attributes configured in Tulip, and the machine ID and attribute ID from the machine configuration page.
* For tables: the table IDs and column names of your Tulip Tables, available in the API documentation of your instance at `https://<instance>.tulip.co/apidocs`.
* Access to the [Admin UI](/2-4-2/access/admin-ui.md) with sufficient [user permissions](/2-4-2/access/user-management.md).
* Basic knowledge of MQTT and the Connectware [services](/2-4-2/data-flows/services.md) concept (for example, [service commissioning files](/2-4-2/data-flows/service-commissioning-files.md), [connections](/2-4-2/data-flows/service-commissioning-files/resources/cybus-connection.md), and [endpoints](/2-4-2/data-flows/service-commissioning-files/resources/cybus-endpoint.md)).

## Connectware and Tulip Integration

Tulip exposes its functionality as REST APIs under `https://<instance>.tulip.co/api/v3`, authenticated with HTTP basic authentication using the credentials of an API token. Connectware communicates with these APIs through the [HTTP/REST connector](/2-4-2/connectors/enterprise-connectors/http-rest.md).

This guide uses the two most common APIs for sending shop floor data to Tulip:

* **Machine Attributes API**: Updates the attribute values of a Tulip machine. Tulip uses these values in machine monitoring, machine triggers, and overall equipment effectiveness (OEE) calculations. This is the standard way to stream live machine data, such as temperature or part counts, into Tulip.
* **Table API**: Creates one table record per message. Tulip Tables store structured data, such as quality events or work orders, that Tulip apps read and write. The same API also lists records, which this guide uses to poll a table back into Connectware.

The MQTT topics in this guide follow an ISA-95-style equipment hierarchy (`<enterprise>/<site>/<area>/<line>/<cell>`). The mappings subscribe with wildcards across all levels, so any machine in the hierarchy is picked up without changing the integration.

### Tulip Connection Properties

The connection to Tulip requires the host name of your instance and the credentials of your API token. We add them as parameters to the service commissioning file, so you can set them when you install the service.

Do not worry about copying the service commissioning file snippets together into one, the complete example file is available at the end of this guide.

* `tulipHost`: The host name of your Tulip instance, without the scheme. For example, `example.tulip.co`.
* `apiKey` and `apiSecret`: The credentials of your Tulip API token.
* `machineId` and `temperatureAttributeId`: The IDs of the Tulip machine and machine attribute that receive the temperature values.
* `qualityTableId` and `workOrdersTableId`: The IDs of the Tulip Tables for quality events and work orders.
* `topicRoot`: The root of the MQTT topic hierarchy. Defaults to `enterprise`.

{% code lineNumbers="true" %}

```yaml
parameters:
  tulipHost:
    description: Host name of your Tulip instance
    type: string
    default: example.tulip.co

  apiKey:
    description: API key of the Tulip API token, used as the basic auth username
    type: string

  apiSecret:
    description: Secret of the Tulip API token, used as the basic auth password
    type: string

  machineId:
    description: ID of the Tulip machine that receives the temperature values
    type: string

  temperatureAttributeId:
    description: ID of the machine attribute that receives the temperature values
    type: string

  qualityTableId:
    description: ID of the Tulip Table that stores quality events
    type: string

  workOrdersTableId:
    description: ID of the Tulip Table that contains work orders
    type: string

  topicRoot:
    description: Root of the MQTT topic hierarchy
    type: string
    default: enterprise
```

{% endcode %}

### Tulip Connection

To connect to the Tulip API, we set up a `Cybus::Connection` resource that uses the HTTP/REST connector with [basic authentication](/2-4-2/connectors/enterprise-connectors/http-rest.md#authentication-methods). Tulip API tokens authenticate with HTTP basic authentication: the API key is the username and the secret is the password.

The `prefix` property applies the `/api/v3` base path to all endpoints of the connection, so the endpoint paths stay short.

{% code lineNumbers="true" %}

```yaml
resources:
  tulipConnection:
    type: Cybus::Connection
    properties:
      protocol: Http
      connection:
        scheme: https
        host: !ref tulipHost
        port: 443
        prefix: /api/v3
        auth:
          username: !ref apiKey
          password: !ref apiSecret
```

{% endcode %}

Tulip API tokens are either workspace-scoped or account-scoped. The paths in this guide work for workspace-scoped tokens. For an account-scoped token on an instance with multiple workspaces, include the workspace in the prefix, for example `/api/v3/w/DEFAULT`.

### Streaming Machine Attributes to Tulip

The Machine Attributes API updates the attribute values of a Tulip machine with a POST request to the `/attributes/report` path. Each attribute is identified by the combination of a machine ID and an attribute ID, both shown on the machine configuration page in Tulip. This API requires the `attributes:write` scope.

We define a write endpoint for the API path and a mapping that feeds it from the MQTT topic hierarchy. The HTTP/REST connector expects the request body in the `body` property of the message (see [Publishing Data to REST Servers](/2-4-2/connectors/enterprise-connectors/http-rest.md#publishing-data-to-rest-servers)). The `transform` rule wraps the incoming payload accordingly, so machines can publish their data without knowing about this convention.

{% code lineNumbers="true" %}

```yaml
machineAttributesEndpoint:
  type: Cybus::Endpoint
  properties:
    protocol: Http
    connection: !ref tulipConnection
    write:
      path: /attributes/report

machineAttributesMapping:
  type: Cybus::Mapping
  properties:
    mappings:
      - subscribe:
          topic: !sub '${topicRoot}/+site/+area/+line/+cell/machine-attributes'
        publish:
          endpoint: !ref machineAttributesEndpoint
        rules:
          - transform:
              expression: '{ "body": $ }'
```

{% endcode %}

Any message published to a matching topic, for example `enterprise/hamburg/assembly/line-1/press-01/machine-attributes`, now reports the contained attribute values to Tulip. The payload must follow the request schema of the Machine Attributes API: an `attributes` array in which each element carries a `machineId`, an `attributeId`, and a `value`. One request can report several attributes, even for different machines.

{% code lineNumbers="true" %}

```json
{
  "attributes": [
    {
      "machineId": "yourMachineId",
      "attributeId": "yourAttributeId",
      "value": 42.1
    }
  ]
}
```

{% endcode %}

The type of each `value` must match the data type of the attribute in Tulip. If any attribute fails validation, Tulip processes none of the attributes in the request.

Machines rarely publish payloads in this API-specific structure. In that case, build the structure in the `transform` rule instead. The following mapping reports a plain temperature reading, published as `{ "temperature": 42.1 }`, to one specific machine attribute. Add one mapping like this per machine and attribute pair.

{% code lineNumbers="true" %}

```yaml
temperatureReportMapping:
  type: Cybus::Mapping
  properties:
    mappings:
      - subscribe:
          topic: !sub '${topicRoot}/hamburg/assembly/line-1/press-01/temperature'
        publish:
          endpoint: !ref machineAttributesEndpoint
        rules:
          - transform:
              expression: !sub '{ "body": { "attributes": [ { "machineId": "${machineId}", "attributeId": "${temperatureAttributeId}", "value": temperature } ] } }'
```

{% endcode %}

For more information on transformation expressions, see [Data Processing Rules](/2-4-2/data-flows/rule-engine/data-processing-rules.md).

### Creating Tulip Table Records

The Table API creates one table record per POST request to the `/tables/<tableId>/records` path. This API requires the `tables:write` scope. We use it to store quality events from the shop floor in a Tulip Table, where Tulip apps can display and process them.

{% code lineNumbers="true" %}

```yaml
qualityEventsEndpoint:
  type: Cybus::Endpoint
  properties:
    protocol: Http
    connection: !ref tulipConnection
    write:
      path: !sub '/tables/${qualityTableId}/records'

qualityEventsMapping:
  type: Cybus::Mapping
  properties:
    mappings:
      - subscribe:
          topic: !sub '${topicRoot}/+site/+area/+line/+cell/quality-events'
        publish:
          endpoint: !ref qualityEventsEndpoint
        rules:
          - transform:
              expression: '{ "body": $ }'
```

{% endcode %}

The message payload must match the column schema of the table. The `id` field is the only required field and must be unique within the table. Columns that are not part of the payload are stored as null. Tulip generates a unique prefix for every column name, for example `pcmrk_description`. The exact column names of your table are listed in the API documentation of your instance at `https://<instance>.tulip.co/apidocs`.

{% code lineNumbers="true" %}

```json
{
  "id": "evt-0001",
  "pcmrk_description": "Scratch on housing",
  "wdymc_station": "press-01"
}
```

{% endcode %}

If the publishing system does not provide a unique `id`, generate one in the `transform` rule, for example with `{ "body": $merge([$, { "id": $string($millis()) }]) }`.

### Polling a Tulip Table

To bring Tulip data back to the shop floor, we poll a work orders table with a subscribe endpoint. The HTTP/REST connector sends a GET request to the `/tables/<tableId>/records` path at the configured interval and publishes the response to the MQTT broker (see [Subscribing to Data](/2-4-2/connectors/enterprise-connectors/http-rest.md#subscribing-to-data-continuous-polling)). Listing records requires the `tables:read` scope.

The `limit` query parameter controls how many records one request returns, up to 100. Without it, Tulip returns ten records. The Table API also supports filter and sort parameters, documented in the API documentation of your instance.

{% code lineNumbers="true" %}

```yaml
workOrdersEndpoint:
  type: Cybus::Endpoint
  properties:
    protocol: Http
    connection: !ref tulipConnection
    subscribe:
      path: !sub '/tables/${workOrdersTableId}/records'
      interval: 10000
      query:
        limit: '100'

workOrdersMapping:
  type: Cybus::Mapping
  properties:
    mappings:
      - subscribe:
          endpoint: !ref workOrdersEndpoint
        publish:
          topic: !sub '${topicRoot}/tulip/work-orders'
```

{% endcode %}

Every ten seconds, Connectware publishes a message to the `enterprise/tulip/work-orders` topic. The message contains a `timestamp` property and a `value` property that holds the array of table records.

## Verifying the Integration

1. Install the service and set the parameters with the values from your Tulip instance.
2. Check that the connection is in the **Connected** state on the service details page in the Admin UI.
3. Publish a test message with the Machine Attributes API payload to `enterprise/hamburg/assembly/line-1/press-01/machine-attributes`, for example with an MQTT client or the Admin UI.
4. Open the machine in Tulip and check that the attribute shows the reported value.
5. Publish a quality event to `enterprise/hamburg/assembly/line-1/press-01/quality-events` and check that the Tulip Table contains the new record.
6. Use the [Data Explorer](/2-4-2/monitoring/data-explorer.md) to inspect the polled work orders on the `enterprise/tulip/work-orders` topic.
7. The result of every HTTP request is published to the `/res` topic of the endpoint. If Tulip rejects a request, the message on the `/res` topic contains an `error` property with the HTTP status. A 401 or 403 status indicates wrong API token credentials or missing scopes. A 400 or 422 status indicates a payload that does not match the request schema.

## Service Commissioning File Example

{% file src="/files/zN9SnIOzXILgmtULY0w8" %}

{% code title="tulip-example.yml" lineNumbers="true" expandable="true" %}

```yaml
---
# ----------------------------------------------------------------------------
# Service Commissioning File
# ----------------------------------------------------------------------------
# Copyright: Cybus GmbH
# Contact: support@cybus.io
# ----------------------------------------------------------------------------
# Tulip Integration (Example)
# ----------------------------------------------------------------------------

description: >
  Service commissioning file for the integration between Connectware and the
  Tulip frontline operations platform (Example)

metadata:
  name: Tulip Integration
  provider: cybus
  homepage: https://www.cybus.io
  version: 1.0.0

parameters:
  tulipHost:
    description: Host name of your Tulip instance
    type: string
    default: example.tulip.co

  apiKey:
    description: API key of the Tulip API token, used as the basic auth username
    type: string

  apiSecret:
    description: Secret of the Tulip API token, used as the basic auth password
    type: string

  machineId:
    description: ID of the Tulip machine that receives the temperature values
    type: string

  temperatureAttributeId:
    description: ID of the machine attribute that receives the temperature values
    type: string

  qualityTableId:
    description: ID of the Tulip Table that stores quality events
    type: string

  workOrdersTableId:
    description: ID of the Tulip Table that contains work orders
    type: string

  topicRoot:
    description: Root of the MQTT topic hierarchy
    type: string
    default: enterprise

resources:
  # Connection to the Tulip API using an API token (HTTP basic authentication)
  tulipConnection:
    type: Cybus::Connection
    properties:
      protocol: Http
      connection:
        scheme: https
        host: !ref tulipHost
        port: 443
        # For account-scoped API tokens on multi-workspace instances,
        # include the workspace in the prefix, for example /api/v3/w/DEFAULT
        prefix: /api/v3
        auth:
          username: !ref apiKey
          password: !ref apiSecret

  # Reports machine attribute values to Tulip
  machineAttributesEndpoint:
    type: Cybus::Endpoint
    properties:
      protocol: Http
      connection: !ref tulipConnection
      write:
        path: /attributes/report

  machineAttributesMapping:
    type: Cybus::Mapping
    properties:
      mappings:
        - subscribe:
            topic: !sub '${topicRoot}/+site/+area/+line/+cell/machine-attributes'
          publish:
            endpoint: !ref machineAttributesEndpoint
          rules:
            - transform:
                expression: '{ "body": $ }'

  # Builds the Machine Attributes API payload from a plain temperature value.
  # Add one mapping like this per machine and attribute pair.
  temperatureReportMapping:
    type: Cybus::Mapping
    properties:
      mappings:
        - subscribe:
            topic: !sub '${topicRoot}/hamburg/assembly/line-1/press-01/temperature'
          publish:
            endpoint: !ref machineAttributesEndpoint
          rules:
            - transform:
                expression: !sub '{ "body": { "attributes": [ { "machineId": "${machineId}", "attributeId": "${temperatureAttributeId}", "value": temperature } ] } }'

  # Creates one Tulip Table record per quality event
  qualityEventsEndpoint:
    type: Cybus::Endpoint
    properties:
      protocol: Http
      connection: !ref tulipConnection
      write:
        path: !sub '/tables/${qualityTableId}/records'

  qualityEventsMapping:
    type: Cybus::Mapping
    properties:
      mappings:
        - subscribe:
            topic: !sub '${topicRoot}/+site/+area/+line/+cell/quality-events'
          publish:
            endpoint: !ref qualityEventsEndpoint
          rules:
            - transform:
                expression: '{ "body": $ }'

  # Polls the work orders table and publishes the records to MQTT
  workOrdersEndpoint:
    type: Cybus::Endpoint
    properties:
      protocol: Http
      connection: !ref tulipConnection
      subscribe:
        path: !sub '/tables/${workOrdersTableId}/records'
        interval: 10000
        query:
          limit: '100'

  workOrdersMapping:
    type: Cybus::Mapping
    properties:
      mappings:
        - subscribe:
            endpoint: !ref workOrdersEndpoint
          publish:
            topic: !sub '${topicRoot}/tulip/work-orders'
```

{% endcode %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.cybus.io/2-4-2/guides/system-connectivity/mes-erp-business-systems/tulip-integration.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
