> For the complete documentation index, see [llms.txt](https://docs.informationhub.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.informationhub.io/tutorial/super.md).

# Super: Programmatic Access & Automation

This section covers the most powerful features of Information Hub: direct API access, key generators, form pre-filling, the audit log, webhooks, and dynamic file paths. These features are aimed at users who want to automate workflows, integrate with external systems, or process data programmatically.

{% hint style="info" %}
This section assumes you have completed the previous tutorials and have the **Fynbos in Siberia** project with tables, forms, and dashboards already set up.
{% endhint %}

***

## Step 1: Create an API key

To access data programmatically, you need an API key.

1. Click your profile icon and go to **Settings**.
2. Navigate to the **API Keys** card.
3. Click **Create**.
4. Give it a name (e.g., **Fynbos Analysis Script**).
5. Copy the generated key immediately - it will not be shown again.

{% hint style="warning" %}
Treat your API key like a password. Do not share it publicly or commit it to version control.
{% endhint %}

***

## Step 2: Query data via the GraphQL API

Information Hub exposes a GraphQL API at `https://app.informationhub.io/graphql`. You can use it to read and write data programmatically.

### Query the Species Observations table

Using a tool like `curl`, Postman, or any GraphQL client, send the following query:

```graphql
query {
  queryTable(
    request: {
      tableId: "YOUR_TABLE_ID"
    }
  ) {
    data
  }
}
```

Include your API key in the request headers:

```
Authorization: Bearer YOUR_API_KEY
```

**Using curl:**

```bash
curl -X POST https://app.informationhub.io/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{
    "query": "query { queryTable(request: { tableId: \"YOUR_TABLE_ID\" }) { data } }"
  }'
```

The response contains all rows from the Species Observations table as JSON.

### Find your table ID

The table ID is visible in the URL when you open a table in the browser. For example, if the URL is `https://app.informationhub.io/project/abc123/tables/tbl_456`, then `tbl_456` is the table ID.

***

## Step 3: Insert data via the GraphQL API

You can also add new rows programmatically. This is useful for integrating with sensors, scripts, or external data sources.

```graphql
mutation {
  insertData(
    request: {
      tableId: "YOUR_TABLE_ID"
      data: [
        {
          species_name: "Protea siberica"
          site: "Site Alpha"
          height_cm: 52.3
          leaf_count: 42
          soil_ph: 5.8
          observation_date: "2026-04-01"
          observer: "Automated Sensor"
        }
      ]
    }
  ) {
    data
  }
}
```

**Using curl:**

```bash
curl -X POST https://app.informationhub.io/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{
    "query": "mutation { insertData(request: { tableId: \"YOUR_TABLE_ID\", data: [{ species_name: \"Protea siberica\", site: \"Site Alpha\", height_cm: 52.3, leaf_count: 42, soil_ph: 5.8, observation_date: \"2026-04-01\", observer: \"Automated Sensor\" }] }) { data } }"
  }'
```

After running this mutation, open the Species Observations table in the browser - the new row should be there.

***

## Step 4: Generate R and Python analysis scripts

The Analyse tool can generate starter scripts pre-configured with your table ID and API key.

1. Open the **Species Observations** table.
2. Click **Analyse** in the toolbar.
3. Click the language toggle to switch between **Python** and **R**.
4. Click the **copy** icon to copy the generated script.

### Example generated Python script

```python
import requests
import pandas as pd

API_URL = "https://app.informationhub.io/graphql"
API_KEY = "YOUR_API_KEY"
TABLE_ID = "YOUR_TABLE_ID"

headers = {
    "Content-Type": "application/json",
    "Authorization": f"Bearer {API_KEY}"
}

query = """
query {
  queryTable(request: { tableId: "%s" }) {
    data
  }
}
""" % TABLE_ID

response = requests.post(API_URL, json={"query": query}, headers=headers)
data = response.json()["data"]["queryTable"]["data"]

df = pd.DataFrame(data)
result = df.groupby("species_name")["height_cm"].mean()
print(result)
```

### Example generated R script

```r
library(httr)
library(jsonlite)

api_url <- "https://app.informationhub.io/graphql"
api_key <- "YOUR_API_KEY"
table_id <- "YOUR_TABLE_ID"

query <- sprintf('{
  "query": "query { queryTable(request: { tableId: \\"%s\\" }) { data } }"
}', table_id)

response <- POST(
  api_url,
  add_headers(
    "Content-Type" = "application/json",
    "Authorization" = paste("Bearer", api_key)
  ),
  body = query
)

data <- fromJSON(content(response, "text"))$data$queryTable$data
df <- as.data.frame(data)

aggregate(height_cm ~ species_name, data = df, FUN = mean)
```

Replace the placeholder values with your actual API key and table ID, then run the script in your local environment.

***

## Step 5: Key generators

A **key generator** auto-populates a table's primary key column with values based on a template pattern. Instead of auto-incrementing integers (1, 2, 3...), rows get human-readable IDs like `OBS-20260501-042`.

Key generators are project-level resources - create one and reuse it across multiple tables.

### Create a key generator

1. Click **Tables** in the project sidebar.
2. Click the **Key Generators** button in the toolbar (top-right area of the tables list page).
3. Click **+** to create a new generator.
4. Enter the name **Observation ID**.
5. In the **Template** field, enter: `OBS-{date:YYYYMMDD}-{autoincrement}`
6. Click **Create**.

### Assign it to the primary key column

1. Open the **Species Observations** table.
2. Click the **pencil icon** on the `id` column header to edit it.
3. In the **Key Generator** dropdown, select **Observation ID**.
4. Click **Save**.

New rows added to this table will now receive primary keys like `OBS-20260501-29` instead of plain integers. Existing rows keep their original IDs.

### Token reference

Templates are strings that mix literal text with tokens in `{...}` syntax.

| Token             | Output                                                                | Example                                |
| ----------------- | --------------------------------------------------------------------- | -------------------------------------- |
| `{autoincrement}` | Incrementing integer                                                  | `42`                                   |
| `{uuid}`          | Random v4 UUID                                                        | `f47ac10b-58cc-4372-a567-0e02b2c3d479` |
| `{cuid}`          | Collision-resistant unique ID                                         | `clh3z8k0v0000qzrm...`                 |
| `{date}`          | Current date as `YYYY-MM-DD`                                          | `2026-05-26`                           |
| `{date:FORMAT}`   | Date in a custom moment.js format                                     | `{date:YYYYMMDD}` → `20260526`         |
| `{time}`          | Current time as `HH:mm:ss`                                            | `14:32:07`                             |
| `{time:FORMAT}`   | Time in a custom format                                               | `{time:HHmm}` → `1432`                 |
| `{datetime}`      | Date and time (ISO 8601)                                              | `2026-05-26T14:32:07+00:00`            |
| `{$columnName}`   | Value of another column, referenced by its display name (recommended) | `{$site}` → `Site Alpha`               |
| `{columnId}`      | Value of another column, referenced by its internal ID                | `Site Alpha`                           |

**Slicing:** add `[start:end]` after the token name to take only part of the value:

* `{uuid[0:8]}` - first 8 characters of a UUID
* `{cuid[0:12]}` - first 12 characters of a CUID

**Formatters:** add `:lower`, `:upper`, or `:slug`:

* `{uuid:upper}` - UUID in uppercase
* `{$site:slug}` - column value lowercased with spaces replaced by hyphens

### Working examples

**Simple sequential IDs**

```
OBS-{autoincrement}
```

Produces: `OBS-1`, `OBS-2`, `OBS-3`

**Date-prefixed with short UUID**

```
{date:YYYYMMDD}-{uuid[0:8]:upper}
```

Produces: `20260526-F47AC10B`, `20260526-A3B2C1D0`

**Site-prefixed sequential ID** (using the display name of the `site` column)

```
{$site:slug}-{autoincrement}
```

Produces: `site-alpha-1`, `site-beta-2`

**Short CUID**

```
SP-{cuid[0:8]}
```

Produces: `SP-clh3z8k0`, `SP-clh4a9m1`

{% hint style="info" %}
To find a column's ID for use in a template, look at the URL when editing the column in the table settings. Column IDs are 20-40 alphanumeric characters.
{% endhint %}

{% hint style="warning" %}
Key generators can only be assigned to **primary key columns**. They apply to new rows only - existing rows keep their original primary keys.
{% endhint %}

***

## Step 6: URL query parameter pre-filling

Dr. Mwangi sends her two field teams to different sites each day. Instead of asking each team member to select their site from a dropdown, she sends them a pre-filled link where the site is already set.

### Configure the question

1. Open the **Field Observation Form** in the form builder.
2. Click the **pencil icon** on the **site** question to edit it.
3. Click the **cog icon** to open the expanded configuration panel.
4. Enter `site` in the **URL Query Param Key** field.
5. Click **Save**.

### Create pre-filled links

Now append the parameter to the share link:

* For Site Alpha: `https://app.informationhub.io/form/YOUR_FORM_ID?site=Site+Alpha`
* For Site Beta: `https://app.informationhub.io/form/YOUR_FORM_ID?site=Site+Beta`

When Dr. Tanaka opens the Site Alpha link, the **site** field is pre-filled with `Site Alpha`. She can still change it if needed (unless the question is also set to **Locked**).

### Combining multiple parameters

You can pre-fill multiple fields at once by chaining parameters:

```
.../form/YOUR_FORM_ID?site=Site+Alpha&observer=Dr.+Tanaka
```

This requires a **URL Query Param Key** set on each question you want to pre-fill. Parameters that do not match any key are silently ignored.

**Use cases:**

* Send site-specific links to each field team so the site is never wrong
* Pre-fill the observer name for each team member's personal link
* Pre-fill a batch ID from an external system by embedding it in a redirect URL

***

## Step 7: Table audit log and rollback

Every change to a row in a table is recorded in the audit log. This gives you a full history of edits and the ability to roll back a row to any previous state.

### View the audit log

1. Open the **Species Observations** table.
2. Click the **Settings** button in the toolbar.
3. The settings page has an **Audit Log** link in the sidebar - click it. (Alternatively, navigate to `/project/:id/tables/:tableId/audit` directly.)
4. The log shows one entry per row change, with the before and after values of each field. Click an entry to expand and see the field-level diff.

### Roll back a row

If Dr. Volkov accidentally overwrites a correct observation with wrong values, he can roll it back:

1. Find the history entry for the incorrect edit - it shows the timestamp, the user who made the change, and which fields changed.
2. Click **Rollback** on that entry.
3. The row is restored to the state it was in just before that edit.

{% hint style="info" %}
Rollback requires the appropriate permission (`tables.data.rollback` or equivalent in the project's role configuration). Check with your project administrator if the button is not visible.
{% endhint %}

***

## Step 8: Dynamic file upload paths

When forms include file upload questions, uploaded files go to Storage. By default, they all land in the same folder. Dynamic paths let you organise uploads automatically by the values in other form fields.

1. Open the **Field Observation Form** in the form builder.
2. Click the **pencil icon** on the **Plant Photo** question.
3. Turn on the **Use Dynamic Path Template** toggle.
4. In the **Upload Path Template** field, enter (note the `$` prefix, which references a column by its display name):

```
observations/{$site}/{$species_name}/{$observation_date}
```

Now when a field worker uploads a photo:

* An observation at Site Alpha for Protea siberica on 2026-04-01 saves to: `observations/Site Alpha/Protea siberica/2026-04-01/`
* An observation at Site Beta for Erica glacialis on 2026-04-02 saves to: `observations/Site Beta/Erica glacialis/2026-04-02/`

The template can reference any column in the linked table. This keeps Storage organised as the volume of uploads grows.

{% hint style="warning" %}
Every column named in the template must have a value when the form is submitted. If Dr. Mwangi references a column that is hidden or optional and it is left empty, the upload cannot build its path and the submission fails. Give such questions a **default value** so the path always resolves. See [Dynamic file upload paths](/project/tables/create-table.md#dynamic-file-upload-paths) for the full token list.
{% endhint %}

***

## Step 9: Webhooks - trigger external actions

Webhooks let you notify an external system every time a form is submitted. Dr. Mwangi wants to send each new observation to a data processing pipeline.

### Configure the webhook

1. Open the **Field Observation Form** and click **Settings** in the toolbar.
2. In the **General settings** section, find the **Webhook** field.
3. Enter the webhook URL (e.g., `https://your-api.example.com/observations/webhook`).
4. Click **Save**.

### How it works

Every time someone submits the form and a new row is created, Information Hub sends an HTTP POST request to the webhook URL with a JSON body describing the submission. The submitted values are listed under `rows[].columns[]`, each as a `columnId` / `value` pair - answers are identified by their internal **column ID**, not the column's display name, and every value is sent as text.

Example payload:

```jsonc
{
  "tableId": "tbl_abc123",
  "rows": [
    {
      "columns": [
        { "columnId": "col_species", "value": "Protea siberica" },
        { "columnId": "col_site", "value": "Site Alpha" },
        { "columnId": "col_height", "value": "52.3" },
        { "columnId": "col_date", "value": "2026-04-01" }
      ]
    }
  ],
  "formId": "frm_def456",
  "formName": "Field Observation Form",
  "projectId": "prj_ghi789",
  "actorId": "usr_jkl012",
  "submitterEmail": "lena.mwangi@example.com"
}
```

The request also sends the submitter's authentication token in the `authorization` header, so your endpoint can verify who submitted it. Because answers are keyed by column ID, your receiver needs to map those IDs to your own field names - you can find a column's ID from its edit dialog.

{% hint style="info" %}
The webhook is sent once with no automatic retry, and a failure does not affect the submission - the row is still saved even if your endpoint is unreachable. Editing an existing row does not fire the webhook; only new-row submissions do. A form that writes to several tables sends one request per table.
{% endhint %}

**What you can do with webhooks:**

* Send data to a cloud function for processing or validation
* Trigger notifications in Slack, Teams, or email
* Push data to an external database or analytics platform
* Start an automated analysis pipeline

***

## Step 10: IoT sensor data collection

The research team deploys a network of environmental sensors across the Siberian field sites to record temperature, humidity, and soil moisture automatically. Rather than mixing automated sensor readings with manual observations in the same project, Dr. Mwangi creates a dedicated project for the sensor data.

### Set up a second project for sensor data

1. From the Home page, click **+ Create project**.
2. Name it **Siberia Climate Network** and click **Create**.
3. Inside the new project, go to **Settings** and turn on the **Sensors** toggle, labelled **Enable IoT sensors (TTN) for this project**. This adds a **Sensors** item to the project sidebar.

### Create the sensor metadata table

The Sensors feature needs a metadata table with one row per physical device, telling Information Hub which table (or tables) each device's readings should go to and how to read the data. Create a table called **Sensor Devices** with at least these columns:

| Column       | Type                    | Notes                                                                                                                                                                                              |
| ------------ | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `device_eui` | Text (required, unique) | The device's unique EUI from TTN, for example `0018B20000001234`                                                                                                                                   |
| `tables`     | Text (required)         | One destination table, or several separated by commas, where readings for this device are stored                                                                                                   |
| `parser`     | Text                    | The parser to use: `decoded_payload` for most devices, `message_default_parser` for SNOET, Dragino, Sentrius, Smart Room, and Milesight devices, or `sensecap_default_parser` for SenseCAP devices |

You can also add an optional `device_id` column to give each device a human-readable name.

{% hint style="warning" %}
The column names must match exactly: `device_eui`, `tables`, `parser`. The `device_eui` column must be marked both **required** and **unique**, and the `tables` column must be marked **required**, or validation will fail in the next step.
{% endhint %}

### Create the readings table

Create a second table called **Climate Readings** to hold the actual sensor data:

| Column          | Type             | Notes                          |
| --------------- | ---------------- | ------------------------------ |
| `device_eui`    | Text             | Which sensor sent this reading |
| `temperature_c` | Double Precision | Temperature in Celsius         |
| `humidity_pct`  | Double Precision | Relative humidity percentage   |
| `soil_moisture` | Double Precision | Soil moisture (volumetric)     |
| `timestamp`     | Timestamp        | When the reading was taken     |

### Configure the Sensors feature

1. Click **Sensors** in the project sidebar. This opens the **Setup** section the first time, since no devices have been seen yet.
2. Under **Step 1 - Sensor Metadata Table**, type into the **Metadata Table** field and select **Sensor Devices** from the matching results.
3. Click **Validate**. Information Hub checks that the table has the right columns and settings and shows either **Table structure is valid** or a list of validation errors to fix.
4. Once validation passes, click **Save**.
5. Under **Step 2 - TTN Webhook**, find the **Endpoint URL - paste into TTN "Base URL"** field. Click its copy icon to copy the URL.
6. Click **Regenerate Token**. This creates the webhook's authentication token (the same button is used later if you ever need to replace the token).
7. Copy the token from the **Bearer Token - paste into TTN "Authorization" header** field using its copy icon.

### Configure TTN to push data to Information Hub

In your TTN console, open the application that manages your Siberian field sensors:

1. Go to **Integrations** - **Webhooks** and click **+ Add webhook**.
2. Choose **Custom webhook**.
3. Set the **Base URL** to the endpoint URL you copied from Information Hub.
4. Add a header: key `Authorization`, value `Bearer YOUR_TOKEN` (using the token you copied).
5. Enable the **Uplink message** event and save the webhook.

For full details on TTN webhook configuration, see the [TTN webhook documentation](https://www.thethingsindustries.com/docs/integrations/webhooks/).

### Register your devices

In the **Sensor Devices** table, add a row for each physical sensor:

| device\_eui      | tables           | parser           |
| ---------------- | ---------------- | ---------------- |
| 0018B20000001234 | Climate Readings | decoded\_payload |
| 0018B20000005678 | Climate Readings | decoded\_payload |

The `decoded_payload` parser reads the uplink payload that TTN decodes using your application's uplink formatter. Each field in the decoded payload is mapped to a column in the destination table (or tables, if you listed more than one in the `tables` column).

{% hint style="info" %}
If a sensor is not yet registered when it sends data, it shows up in the **Devices** section of the Sensors feature with an **Add to Metadata Table** button. Clicking it opens a form pre-filled with the device's EUI where you can pick the target table and parser and register it without typing the EUI by hand.
{% endhint %}

### Verify data is arriving

Leave the setup for a few minutes. When a sensor transmits, TTN sends the decoded payload to Information Hub's webhook. Open the **Climate Readings** table - new rows should appear automatically, one per uplink message.

You can also click **Devices** in the Sensors sidebar to see every device that has sent data, along with its recent delivery failures and last successful reading. If a device fails to deliver data 10 times in a row, Information Hub pauses it automatically (shown as **Blacklisted**) and sends the project a push notification. Toggle the device back on from this screen once the problem is fixed.

### Map field names to columns (optional)

If your sensor's decoded field names do not match your column names exactly, click **Column Maps** in the Sensors sidebar. Add the target table, then add a row for each field you want to capture, for example mapping the sensor field `temp` to your `temperature_c` column. Saving replaces all mappings for that table, so include every mapping you want to keep.

***

## Step 11: Analytics with Metabase

With field observation data in the main **Fynbos in Siberia** project and climate readings accumulating automatically in **Siberia Climate Network**, Dr. Mwangi wants to build dashboards that show both datasets together - for example, whether soil moisture levels correlate with Fynbos height measurements at each site.

### Enable Analytics in the Fynbos project

1. Open the **Fynbos in Siberia** project.
2. Go to **Settings** and turn on the **Analytics** toggle, labelled **Enable analytics (Metabase) for this project**. Only a project administrator can see and use this toggle.
3. An **Analytics** item appears in the project sidebar (do not confuse it with the existing **Dashboards** item - **Dashboards** is Information Hub's own dashboard tool, while **Analytics** opens the separate Metabase workspace). Information Hub sets up the Metabase workspace in the background, which can take a short while the first time.

All of your project's tables (Species Observations, Sites, and so on) are automatically available as data sources in Metabase - there is no per-table setting to turn on.

### Open the Metabase workspace

1. Click **Analytics** in the project sidebar.
2. Click **Open in Metabase**. This opens Metabase in a new browser tab, signed in as you, with access to your project's data.

### Connect climate data from the sensor project

To query climate readings from the **Siberia Climate Network** project in the same dashboard, request a data connection between the two projects:

1. On the **Analytics** page in **Fynbos in Siberia**, find the **Data Connections** card.
2. Enter the **Siberia Climate Network** project's ID in the **Source project ID** field and click **Request Access**.
3. An administrator of **Siberia Climate Network** receives a notification and opens the request from their own project's **Analytics** page, where they can **Accept** or **Reject** it.
4. Once accepted, the connection shows as **ACTIVE**, and the Climate Readings table becomes visible as a data source in your Metabase workspace, under a schema named after the Siberia Climate Network project.

{% hint style="info" %}
Data connections are read-only. Either project can end the connection at any time from its own **Analytics** page.
{% endhint %}

### Build a combined dashboard

With both data sources connected, create a dashboard directly in Metabase:

1. In the Metabase workspace, click **+ New** - **Dashboard**.
2. Add a question that queries the Species Observations table - for example, average `height_cm` grouped by `site` and `observation_date`.
3. Add a second question querying Climate Readings - for example, average `temperature_c` and `soil_moisture` grouped by `device_eui` and `timestamp` date.
4. Arrange both charts on the dashboard and add a date filter that controls both queries at once.
5. Click **Save**.

For guidance on building Metabase questions, filters, and dashboards, see the [Metabase documentation](https://www.metabase.com/docs/latest/).

### Share a dashboard

To share a dashboard with collaborators who do not have an Information Hub account:

1. Go back to the **Analytics** page in Information Hub and click **Generate Links**.
2. Information Hub creates a public link for every dashboard in your project's Metabase workspace and lists them under **Dashboard Links**.
3. Find your dashboard in the list and copy its **Public link**, then share it with stakeholders.

### Set up a notification hook

To receive a notification when a saved question's alert condition is met (for example, soil moisture dropping below 20%):

1. On the **Analytics** page, find the **Notification Hooks** card and click **New Hook**.
2. Choose the saved Metabase question to watch and set up its alert condition.
3. Choose whether to be notified by email or push, and who should receive it.
4. Save the hook.

Metabase checks the condition on a schedule; when it fires, Information Hub delivers the notification to the recipients you chose.

***

## What you have learned

* How to create and manage API keys for programmatic access
* How to query and insert table data via the GraphQL API
* How to generate and run R and Python analysis scripts
* How to create key generators with all supported tokens, slices, and formatters
* How to assign a key generator to a primary key column for human-readable row IDs
* How to configure URL query param keys to pre-fill form fields from links
* How to use the table audit log to review row history and roll back edits
* How to configure dynamic file upload paths for automatic Storage organisation
* How to set up webhooks to trigger external systems on form submission
* How to set up a second project to receive IoT sensor data via The Things Network
* How to enable Analytics (Metabase) and connect cross-project data for combined dashboards

***

## Congratulations

You have completed the full Information Hub tutorial. You now know how to:

* **Collect data** with tables, forms, Location questions, File Upload, QR codes, offline mode, and update forms
* **Organise** your team with organisations, groups, and role-based permissions
* **Analyse and visualise** data with the built-in tools, dashboards, and external scripts
* **Share** your work through apps, shared forms, public wiki pages, and the Marketplace
* **Automate** with the GraphQL API, key generators, URL pre-filling, webhooks, and dynamic configuration
* **Audit** your data with row history and rollback
* **Integrate IoT sensors** via The Things Network to ingest device data automatically
* **Build analytics** with Metabase, including cross-project data connections and shareable dashboards

For detailed reference on any feature, see the [Home](/home.md) and [Project](/project.md) documentation sections.
