> ## Documentation Index
> Fetch the complete documentation index at: https://docs.coreflux.org/llms.txt
> Use this file to discover all available pages before exploring further.

# ADS Route (Beckhoff)

> Connect to Beckhoff TwinCAT PLCs using ADS protocol with AutoDiscovery and comprehensive data type support

## ADS Overview

The `ADS` route enables communication with Beckhoff TwinCAT PLCs and systems using the ADS (Automation Device Specification) protocol. It supports automatic symbol discovery, bidirectional communication, and complex data types including arrays and structures.

> [!TIP]
>
> ADS provides direct, high-performance access to TwinCAT variables. The AutoDiscovery feature can automatically find and expose all PLC symbols as MQTT topics.

> [!WARNING]
>
> The ADS route is on beta testing. It is planned for a future release.

## Basic Syntax

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE BeckhoffPLC WITH TYPE ADS
    ADD ADS_CONFIG
        WITH LOCAL_NET_ID "192.168.1.10.1.1"
        WITH LOCAL_ROUTE_TO_TARGET "CX_12345"
        WITH TARGET_NET_ID "192.168.1.100.1.1"
        WITH TARGET_IP "192.168.1.100"
        WITH TARGET_PORT 851
        WITH TARGET_ROUTE_TO_LOCAL "MQTTBridge"
    ADD MAPPING ProcessData
        WITH EVERY 500 MILLISECONDS
        ADD TAG Temperature
            WITH ADDRESS "GVL_Process.rTemperature"
            WITH DATA_TYPE "REAL"
            WITH SOURCE_TOPIC "plc/temperature"
```

***

## AMS Net ID Basics

ADS communication requires AMS Net IDs for both local and target systems. The AMS Net ID is typically the IP address followed by `.1.1`.

| Component | Format | Example |
| - | - | - |
| AMS Net ID | `x.x.x.x.y.y` | `192.168.1.100.1.1` |
| IP portion | First 4 octets | `192.168.1.100` |
| Node portion | Last 2 octets | `.1.1` (usually) |

> [!NOTE]
>
> You can find your TwinCAT system's AMS Net ID in TwinCAT XAE under **SYSTEM** → **Routes** or by running `adsInfo` on the target.

***

## Connection Configuration

### ADS\_CONFIG Parameters

**LOCAL\_NET\_ID** `string` *required*

Local AMS Net ID (format: x.x.x.x.x.x). This is typically your machine's IP + .1.1.

**LOCAL\_ROUTE\_TO\_TARGET** `string` *required*

Route name on local machine to reach the target PLC.

**LOCAL\_HOST\_NAME** `string`

Local host name (optional).

**TARGET\_NET\_ID** `string` *required*

Target PLC's AMS Net ID (format: x.x.x.x.x.x).

**TARGET\_IP** `string` *required*

Target PLC's IP address.

**TARGET\_PORT** `integer`

ADS port. Typically 851 for TwinCAT 3, 801 for TwinCAT 2.

**TARGET\_ROUTE\_TO\_LOCAL** `string` *required*

Route name on target PLC to reach the local machine.

**TARGET\_USER** `string`

Username for route creation (typically Administrator).

**TARGET\_PASSWORD** `string`

Password for route creation.

**TIMEOUT** `integer`

Connection timeout in milliseconds. Default: 5000.

### Connection Example

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD ADS_CONFIG
    WITH LOCAL_NET_ID "192.168.1.10.1.1"
    WITH LOCAL_ROUTE_TO_TARGET "PlcRoute"
    WITH TARGET_NET_ID "192.168.1.100.1.1"
    WITH TARGET_IP "192.168.1.100"
    WITH TARGET_PORT 851
    WITH TARGET_ROUTE_TO_LOCAL "MQTTServer"
    WITH TARGET_USER "Administrator"
    WITH TARGET_PASSWORD "1"
    WITH TIMEOUT 5000
```

***

## AutoDiscovery

AutoDiscovery automatically finds all PLC symbols and exposes them as MQTT topics.

### AutoDiscovery Parameters

**AUTO\_DISCOVERY** `boolean`

Enable automatic symbol discovery. Default: false.

**DISCOVERY\_BASE\_TOPIC** `string`

Base MQTT topic for discovered symbols. Default: ads/discovery.

**DISCOVERY\_DEFAULT\_QOS** `integer`

Default QoS for discovered symbols (0-2). Default: 0.

**DISCOVERY\_DEFAULT\_RETAIN** `boolean`

Default retain flag for discovered symbols. Default: false.

**DISCOVERY\_DEFAULT\_TRIGGER** `integer`

Default trigger mode:

* `0` - OnStart (read once at connection)
* `1` - OnChange (publish on value change)
* `2` - Cyclic (poll at interval)

Default: 2.

**DISCOVERY\_DEFAULT\_CYCLE** `integer`

Default polling cycle in milliseconds. Default: 1000.

**DISCOVERY\_FILTER\_INCLUDES** `string`

Include patterns (comma-separated, \* wildcard). Default: \* (all symbols).

**DISCOVERY\_FILTER\_EXCLUDES** `string`

Exclude patterns (comma-separated, \* wildcard).

**DISCOVERY\_MAX\_DEPTH** `integer`

Structure expansion depth. Default: 3.

**DISCOVERY\_PARALLEL\_READING** `boolean`

Enable parallel reading for large projects. Default: false.

**DISCOVERY\_BATCH\_SIZE** `integer`

Batch size for parallel reading. Default: 50.

### AutoDiscovery Example

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE BeckhoffAutoDiscover WITH TYPE ADS
    ADD ADS_CONFIG
        WITH LOCAL_NET_ID "192.168.1.10.1.1"
        WITH LOCAL_ROUTE_TO_TARGET "CX_Route"
        WITH TARGET_NET_ID "192.168.1.100.1.1"
        WITH TARGET_IP "192.168.1.100"
        WITH TARGET_PORT 851
        WITH TARGET_ROUTE_TO_LOCAL "MQTTBridge"
        WITH AUTO_DISCOVERY "true"
        WITH DISCOVERY_BASE_TOPIC "beckhoff/plc"
        WITH DISCOVERY_FILTER_INCLUDES "GVL_*"
        WITH DISCOVERY_FILTER_EXCLUDES "*_Internal*"
        WITH DISCOVERY_DEFAULT_CYCLE 500
        WITH DISCOVERY_MAX_DEPTH 3
```

***

## TwinCAT Data Types

| Data Type | TwinCAT Type | Size | Description |
| - | - | - | - |
| `BOOL` | BOOL | 1 bit | Boolean value |
| `BYTE` | BYTE | 8 bits | Unsigned 8-bit |
| `SINT` | SINT | 8 bits | Signed 8-bit integer |
| `USINT` | USINT | 8 bits | Unsigned 8-bit integer |
| `WORD` | WORD | 16 bits | Unsigned 16-bit |
| `INT` | INT | 16 bits | Signed 16-bit integer |
| `UINT` | UINT | 16 bits | Unsigned 16-bit integer |
| `DWORD` | DWORD | 32 bits | Unsigned 32-bit |
| `DINT` | DINT | 32 bits | Signed 32-bit integer |
| `UDINT` | UDINT | 32 bits | Unsigned 32-bit integer |
| `LWORD` | LWORD | 64 bits | Unsigned 64-bit |
| `LINT` | LINT | 64 bits | Signed 64-bit integer |
| `ULINT` | ULINT | 64 bits | Unsigned 64-bit integer |
| `REAL` / `FLOAT` | REAL | 32 bits | 32-bit floating point |
| `LREAL` / `DOUBLE` | LREAL | 64 bits | 64-bit floating point |
| `CHAR` | CHAR | 8 bits | Single character |
| `WCHAR` | WCHAR | 16 bits | Wide character |
| `TIME` | TIME | 32 bits | Time duration |
| `DATE` | DATE | 16 bits | Date value |
| `TOD` | TOD | 32 bits | Time of day |
| `STRING` | STRING | Variable | ASCII string; sized with `LENGTH` |
| `ARRAY` | ARRAY | Variable | Array of elements (requires `ARRAY_ELEMENT_TYPE`) |
| `STRUCT` | STRUCT | Variable | Structured data type |

### String sizing

For `STRING` tags, `WITH LENGTH` is the total size in **bytes**, **including the ADS length byte**. It defaults to **256** bytes for reads; writes use an **80**-byte default when no size is supplied. `STRING_SIZE` is accepted as an alias for the same field.

`ARRAY_ELEMENT_LENGTH` is a separate setting for `ARRAY` tags whose elements are `STRING` or `STRUCT` — it does not replace `LENGTH` on scalar string tags.

Read a TwinCAT `STRING` symbol with an explicit read size (256 bytes here; writes default to 80 bytes when `LENGTH` is omitted):

```lot wrap focus={15} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE TwinCATStrings WITH TYPE ADS
    ADD ADS_CONFIG
        WITH LOCAL_NET_ID "192.168.1.10.1.1"
        WITH LOCAL_ROUTE_TO_TARGET "CX_Route"
        WITH TARGET_NET_ID "192.168.1.100.1.1"
        WITH TARGET_IP "192.168.1.100"
        WITH TARGET_PORT 851
        WITH TARGET_ROUTE_TO_LOCAL "MQTTBridge"
    ADD MAPPING Labels
        WITH EVERY 1 SECOND
        ADD TAG ProductLabel
            WITH ADDRESS "GVL_Process.sProductName"
            WITH ADDRESS_TYPE "SYMBOL"
            WITH DATA_TYPE "STRING"
            WITH LENGTH 256
            WITH SOURCE_TOPIC "production/label"
```

***

## TAG Configuration

### Symbol-Based Addressing

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD TAG Temperature
    WITH ADDRESS "GVL_Process.rTemperature"
    WITH ADDRESS_TYPE "SYMBOL"
    WITH DATA_TYPE "REAL"
    WITH SOURCE_TOPIC "plc/temperature"
```

### Index-Based Addressing

For direct memory access using index group and offset:

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD TAG DirectValue
    WITH ADDRESS "16448.0"
    WITH ADDRESS_TYPE "INDEX"
    WITH DATA_TYPE "DINT"
    WITH SOURCE_TOPIC "plc/direct"
```

### Array Handling

**ARRAY\_ELEMENT\_TYPE** `string`

Array element type (required for ARRAY): BOOL, INT, REAL, STRING, STRUCT, etc.

**ARRAY\_SIZE** `integer`

Number of elements in the array.

**ARRAY\_ELEMENT\_LENGTH** `integer`

Length of each element in bytes (for STRING/STRUCT arrays).

**ARRAY\_START\_OFFSET** `integer`

Offset in bytes to skip before array data. Default: 0.

**ARRAY\_ELEMENT\_HEADER\_SIZE** `integer`

Header bytes to skip per element. Default: 0.

### Structure Handling

**STRUCT\_FIELD\_DEFINITIONS** `string`

Structure field definitions in format: `name:offset:type:size` (comma-separated).

### String sizing parameter

**LENGTH** `integer`

Total string size in bytes, including the ADS length byte. Default: **256** for reads; **80** for writes when omitted. Alias: `STRING_SIZE`.

***

## Event-Based Operations

For on-demand ADS operations (not polling), use the EVENT syntax. Publish a message to SOURCE\_TOPIC to trigger the operation; the route executes it and publishes the result to DESTINATION\_TOPIC.

### Supported Operations

| Operation | Description | Query Parameters |
| - | - | - |
| `READ` | Read a symbol value on demand | `symbol`, `data_type` |
| `WRITE` | Write a value to a symbol | `symbol`, `data_type`, `value` |

### Query Parameters

| Parameter | Description | Example |
| - | - | - |
| `operation` | Operation type: `READ` or `WRITE` | `READ` |
| `symbol` | TwinCAT symbol path | `GVL_Process.rTemperature`, `GVL_Control.rSP` |
| `data_type` | TwinCAT data type | `REAL`, `LREAL`, `DINT`, `BOOL`, `INT` |
| `value` | Value to write (WRITE only) | `75.0`, `42`, `true` |

### Read Example

Read a process variable on demand:

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD EVENT ReadSymbol
    WITH SOURCE_TOPIC "ads/commands/read"
    WITH DESTINATION_TOPIC "ads/responses/read"
    WITH QUERY "{operation: READ, symbol: GVL_Process.rTemperature, data_type: REAL}"
```

### Write Example

Write a setpoint on demand:

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD EVENT WriteSetpoint
    WITH SOURCE_TOPIC "ads/commands/write"
    WITH DESTINATION_TOPIC "ads/responses/write"
    WITH QUERY "{operation: WRITE, symbol: GVL_Control.rSP, data_type: REAL, value: 75.0}"
```

***

## Complete Examples

**Basic Symbol Reading**

Read PLC variables using symbol names:

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE TwinCATBasic WITH TYPE ADS
    ADD ADS_CONFIG
        WITH LOCAL_NET_ID "192.168.1.10.1.1"
        WITH LOCAL_ROUTE_TO_TARGET "CX_Route"
        WITH TARGET_NET_ID "192.168.1.100.1.1"
        WITH TARGET_IP "192.168.1.100"
        WITH TARGET_PORT 851
        WITH TARGET_ROUTE_TO_LOCAL "MQTTBridge"
    ADD MAPPING ProcessVariables
        WITH EVERY 500 MILLISECONDS
        ADD TAG Temperature
            WITH ADDRESS "GVL_Process.rTemperature"
            WITH ADDRESS_TYPE "SYMBOL"
            WITH DATA_TYPE "REAL"
            WITH SOURCE_TOPIC "plc/process/temperature"
            WITH UNIT "°C"
        ADD TAG Pressure
            WITH ADDRESS "GVL_Process.rPressure"
            WITH ADDRESS_TYPE "SYMBOL"
            WITH DATA_TYPE "REAL"
            WITH SOURCE_TOPIC "plc/process/pressure"
            WITH UNIT "bar"
        ADD TAG RunningState
            WITH ADDRESS "GVL_Process.bRunning"
            WITH ADDRESS_TYPE "SYMBOL"
            WITH DATA_TYPE "BOOL"
            WITH SOURCE_TOPIC "plc/process/running"
```

**AutoDiscovery**

Automatically discover and expose all GVL variables:

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE TwinCATAutoDiscover WITH TYPE ADS
    ADD ADS_CONFIG
        WITH LOCAL_NET_ID "192.168.1.10.1.1"
        WITH LOCAL_ROUTE_TO_TARGET "CX_Route"
        WITH TARGET_NET_ID "192.168.1.100.1.1"
        WITH TARGET_IP "192.168.1.100"
        WITH TARGET_PORT 851
        WITH TARGET_ROUTE_TO_LOCAL "MQTTBridge"
        WITH AUTO_DISCOVERY "true"
        WITH DISCOVERY_BASE_TOPIC "twincat"
        WITH DISCOVERY_FILTER_INCLUDES "GVL_Production.*,GVL_Process.*"
        WITH DISCOVERY_FILTER_EXCLUDES "*_Debug*,*_Test*"
        WITH DISCOVERY_DEFAULT_CYCLE 500
        WITH DISCOVERY_MAX_DEPTH 2
        WITH DISCOVERY_PARALLEL_READING "true"
        WITH DISCOVERY_BATCH_SIZE 100
```

**Bidirectional Control**

Read process values and write setpoints:

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE TwinCATControl WITH TYPE ADS
    ADD ADS_CONFIG
        WITH LOCAL_NET_ID "192.168.1.10.1.1"
        WITH LOCAL_ROUTE_TO_TARGET "CX_Route"
        WITH TARGET_NET_ID "192.168.1.100.1.1"
        WITH TARGET_IP "192.168.1.100"
        WITH TARGET_PORT 851
        WITH TARGET_ROUTE_TO_LOCAL "MQTTBridge"
    ADD MAPPING ControlLoop
        WITH EVERY 200 MILLISECONDS
        ADD TAG ProcessValue
            WITH ADDRESS "GVL_Control.rPV"
            WITH DATA_TYPE "REAL"
            WITH SOURCE_TOPIC "control/pv"
            WITH UNIT "°C"
        ADD TAG Setpoint
            WITH ADDRESS "GVL_Control.rSP"
            WITH DATA_TYPE "REAL"
            WITH SOURCE_TOPIC "control/sp"
            WITH WRITABLE "true"
            WITH DESTINATION_TOPIC "control/sp/write"
            WITH MIN_VALUE 0
            WITH MAX_VALUE 100
        ADD TAG Output
            WITH ADDRESS "GVL_Control.rOutput"
            WITH DATA_TYPE "REAL"
            WITH SOURCE_TOPIC "control/output"
            WITH UNIT "%"
        ADD TAG AutoMode
            WITH ADDRESS "GVL_Control.bAutoMode"
            WITH DATA_TYPE "BOOL"
            WITH SOURCE_TOPIC "control/auto"
            WITH WRITABLE "true"
            WITH DESTINATION_TOPIC "control/auto/write"
```

**Array Data**

Read array values:

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE TwinCATArrays WITH TYPE ADS
    ADD ADS_CONFIG
        WITH LOCAL_NET_ID "192.168.1.10.1.1"
        WITH LOCAL_ROUTE_TO_TARGET "CX_Route"
        WITH TARGET_NET_ID "192.168.1.100.1.1"
        WITH TARGET_IP "192.168.1.100"
        WITH TARGET_PORT 851
        WITH TARGET_ROUTE_TO_LOCAL "MQTTBridge"
    ADD MAPPING ArrayData
        WITH EVERY 1 SECOND
        ADD TAG TemperatureArray
            WITH ADDRESS "GVL_Data.arTemperatures"
            WITH DATA_TYPE "ARRAY"
            WITH ARRAY_ELEMENT_TYPE "REAL"
            WITH ARRAY_SIZE 10
            WITH SOURCE_TOPIC "plc/temperatures"
            WITH PUBLISH_MODE "JSON"
        ADD TAG StatusBits
            WITH ADDRESS "GVL_Data.arStatus"
            WITH DATA_TYPE "ARRAY"
            WITH ARRAY_ELEMENT_TYPE "BOOL"
            WITH ARRAY_SIZE 16
            WITH SOURCE_TOPIC "plc/status"
```

**Structure Data**

Read structured data:

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE TwinCATStruct WITH TYPE ADS
    ADD ADS_CONFIG
        WITH LOCAL_NET_ID "192.168.1.10.1.1"
        WITH LOCAL_ROUTE_TO_TARGET "CX_Route"
        WITH TARGET_NET_ID "192.168.1.100.1.1"
        WITH TARGET_IP "192.168.1.100"
        WITH TARGET_PORT 851
        WITH TARGET_ROUTE_TO_LOCAL "MQTTBridge"
    ADD MAPPING StructData
        WITH EVERY 500 MILLISECONDS
        ADD TAG MachineData
            WITH ADDRESS "GVL_Machine.stMachineData"
            WITH DATA_TYPE "STRUCT"
            WITH STRUCT_FIELD_DEFINITIONS "Speed:0:REAL:4,Current:4:REAL:4,Status:8:INT:2,ErrorCode:10:WORD:2"
            WITH SOURCE_TOPIC "plc/machine"
            WITH PUBLISH_MODE "JSON"
```

**Combined (Cyclic + On-Demand)**

Continuous monitoring with on-demand read and write events in the same route:

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE FullADSSetup WITH TYPE ADS
    ADD ADS_CONFIG
        WITH LOCAL_NET_ID "192.168.1.10.1.1"
        WITH LOCAL_ROUTE_TO_TARGET "CX_Route"
        WITH TARGET_NET_ID "192.168.1.100.1.1"
        WITH TARGET_IP "192.168.1.100"
        WITH TARGET_PORT 851
        WITH TARGET_ROUTE_TO_LOCAL "MQTTBridge"
    
    ADD MAPPING ProcessVariables
        WITH EVERY 500 MILLISECONDS
        ADD TAG Temperature
            WITH ADDRESS "GVL_Process.rTemperature"
            WITH ADDRESS_TYPE "SYMBOL"
            WITH DATA_TYPE "REAL"
            WITH SOURCE_TOPIC "plc/temperature"
            WITH UNIT "°C"
        ADD TAG Pressure
            WITH ADDRESS "GVL_Process.rPressure"
            WITH ADDRESS_TYPE "SYMBOL"
            WITH DATA_TYPE "REAL"
            WITH SOURCE_TOPIC "plc/pressure"
            WITH UNIT "bar"
    
    ADD EVENT ReadSymbol
        WITH SOURCE_TOPIC "ads/commands/read"
        WITH DESTINATION_TOPIC "ads/responses/read"
        WITH QUERY "{operation: READ, symbol: GVL_Process.rOutput, data_type: REAL}"
    
    ADD EVENT WriteSetpoint
        WITH SOURCE_TOPIC "ads/commands/write"
        WITH DESTINATION_TOPIC "ads/responses/write"
        WITH QUERY "{operation: WRITE, symbol: GVL_Control.rSP, data_type: REAL, value: 0}"
```

The MAPPING continuously reads temperature and pressure. To trigger an on-demand read of the output value, publish any message to `ads/commands/read`. To write a setpoint, publish to `ads/commands/write`.

***

## Route Setup Process

To establish ADS communication, routes must be configured on both sides:

### 1. On TwinCAT System

1. Open TwinCAT XAE
2. Go to **SYSTEM** → **Routes**
3. Add a static route:
   * **Route Name**: Match `TARGET_ROUTE_TO_LOCAL` parameter
   * **AMS Net ID**: Your Coreflux machine's AMS Net ID
   * **Transport Type**: TCP/IP
   * **Address**: Your Coreflux machine's IP

### 2. In Coreflux Configuration

The route automatically creates a corresponding route on the local machine using the configured parameters.

> [!WARNING]
>
> Both route names (`LOCAL_ROUTE_TO_TARGET` and `TARGET_ROUTE_TO_LOCAL`) must match the actual route names configured on each system.

***

## Troubleshooting

> **Connection Failed**
>
> * Verify AMS Net IDs are correct on both sides
> * Check routes are properly configured in TwinCAT
> * Ensure TARGET\_PORT is correct (851 for TC3, 801 for TC2)
> * Verify firewall allows ADS port (typically 48898)
> * Check TARGET\_USER/PASSWORD for route creation

> **Symbol Not Found**
>
> * Verify symbol path matches exactly (case-sensitive)
> * Check symbol exists in PLC project
> * Ensure PLC is in RUN mode
> * Use AutoDiscovery to find correct symbol names

> **AutoDiscovery Issues**
>
> * Check DISCOVERY\_FILTER\_INCLUDES patterns
> * Verify DISCOVERY\_MAX\_DEPTH is sufficient
> * For large projects, enable DISCOVERY\_PARALLEL\_READING
> * Check PLC project is compiled and deployed

> **Write Operations Fail**
>
> * Ensure WRITABLE is set to "true"
> * Verify PLC variable is not constant
> * Check value is within MIN\_VALUE/MAX\_VALUE
> * Verify PLC is in RUN mode

> **Timeout Errors**
>
> * Increase TIMEOUT value
> * Check network connectivity
> * Verify TwinCAT runtime is running
> * Reduce DISCOVERY\_BATCH\_SIZE if using AutoDiscovery

***

## Next Steps

> **[Allen-Bradley](./allen-bradley)**
>
> Connect to Rockwell Automation PLCs.

> **[OPC UA Client](./opcua-client)**
>
> Cross-platform OPC UA communication.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.