---
title: "Tables"
---

Tables are the built-in structured data store behind `core.table.*`.
Use them when your workflows need durable, queryable records such as asset inventories, user allowlists, enrichment results, or investigation evidence.

## What tables are good for

- Persisting enrichment data across workflow runs
- Looking up records by a known field such as hostname, email, or indicator
- Searching and exporting structured data for analysts
- Attaching case context to reusable datasets

## Common workflow pattern

Most table workflows follow the same lifecycle:

1. Create the table once with a schema that fits your data.
2. Insert or upsert rows as new events arrive.
3. Look up, search, or export rows later from another workflow step.

```yaml
- ref: ensure_inventory_table
  action: core.table.create_table
  args:
    name: asset_inventory
    columns:
      - name: hostname
        type: TEXT
      - name: owner
        type: TEXT
      - name: last_seen
        type: TIMESTAMPTZ
    raise_on_duplicate: false
- ref: upsert_asset
  action: core.table.insert_row
  args:
    table: asset_inventory
    upsert: true
    row_data:
      hostname: ${{ TRIGGER.hostname }}
      owner: ${{ TRIGGER.owner }}
      last_seen: ${{ FN.now() }}
- ref: find_asset
  action: core.table.lookup
  args:
    table: asset_inventory
    column: hostname
    value: ${{ TRIGGER.hostname }}
```

## Column schema

`core.table.create_table` takes `columns` as a JSON array of column objects.
This is the same schema you use for tables that you later link to cases.

- `name`: Required string. Use letters, numbers, and underscores, and start with a letter or underscore.
- `type`: Required uppercase string. Use `TEXT`, `INTEGER`, `NUMERIC`, `BOOLEAN`, `DATE`, `TIMESTAMPTZ`, `JSONB`, `SELECT`, or `MULTI_SELECT`.
- `nullable`: Optional boolean. Defaults to `true`.
- `default`: Optional value. It must match the column type.
- `options`: Optional array of strings. Required for `SELECT` and `MULTI_SELECT`, and invalid for other types.

The documented `type` values match the custom tables picker.
Case custom fields use the same storage type family: `TEXT`, `INTEGER`, `NUMERIC`, `BOOLEAN`, `DATE`, `TIMESTAMPTZ`, `JSONB`, `SELECT`, and `MULTI_SELECT`.
In the case field picker, raw `JSONB` is currently surfaced through the `URL` kind, and `Long text` is layered on top of `TEXT`.

## Notes

- Use `lookup` or `is_in` when you already know the column and value you need.
- Use `search_rows` when you need broader text search or paginated results.
- Use `download` when you want to export rows as JSON, NDJSON, CSV, or Markdown.

## FAQ

<AccordionGroup>
  <Accordion title="How do I insert more than 1000 rows into a table?">
    `core.table.insert_rows` is best for batch inserts, but you should split large imports into smaller batches first.
    Create batches upstream, then run one `insert_rows` action per batch.

    ```yaml
    - ref: insert_batch
      action: core.table.insert_rows
      for_each: ${{ for var.batch in TRIGGER.row_batches }}
      args:
        table: asset_inventory
        rows_data: ${{ var.batch }}
    ```

    Each `var.batch` should be a list of rows that stays within your chosen batch size.
    This keeps imports predictable and makes retry behavior easier to reason about.
  </Accordion>

  <Accordion title="What column types should I use for tables and case-linked fields?">
    Use the documented uppercase type names: `TEXT`, `INTEGER`, `NUMERIC`, `BOOLEAN`, `DATE`, `TIMESTAMPTZ`, `JSONB`, `SELECT`, and `MULTI_SELECT`.

    - Use `SELECT` and `MULTI_SELECT` only when you also provide `options`.
    - Use `TEXT` or `JSONB` for flexible payloads.
    - Case-linked custom fields follow the same storage type family as tables.

    ```yaml
    - ref: create_table
      action: core.table.create_table
      args:
        name: asset_inventory
        columns:
          - name: hostname
            type: TEXT
          - name: first_seen_at
            type: TIMESTAMPTZ
          - name: owner_team
            type: SELECT
            options:
              - secops
              - platform
              - it
    ```
  </Accordion>
</AccordionGroup>

{/* Auto-generated by scripts/generate_core_action_docs.py; do not edit by hand. */}

## `core.table.create_table`

Create a new lookup table with optional columns.

### Inputs

<ParamField path="name" type="string" required>

The name of the table to create.

</ParamField>

<ParamField path="columns" type="array[object] | null">

List of column definitions. Each item is a JSON object with required `name` and uppercase `type`, plus optional `nullable`, `default`, and `options` fields. Use `options` only with `SELECT` or `MULTI_SELECT`.

Default: `null`.

</ParamField>

<ParamField path="raise_on_duplicate" type="boolean">

If true, raise an error if the table already exists.

Default: `true`.

</ParamField>

### Examples

**Create and inspect a table**

```yaml
- ref: create_table
  action: core.table.create_table
  args:
    name: asset_inventory
    columns:
      - name: hostname
        type: TEXT
      - name: owner
        type: TEXT
    raise_on_duplicate: false
- ref: list_tables
  action: core.table.list_tables
- ref: table_metadata
  action: core.table.get_table_metadata
  args:
    name: asset_inventory
```

## `core.table.list_tables`

Get a list of all available tables in the workspace.

### Inputs

This action does not take input fields.

### Examples

**Create and inspect a table**

```yaml
- ref: create_table
  action: core.table.create_table
  args:
    name: asset_inventory
    columns:
      - name: hostname
        type: TEXT
      - name: owner
        type: TEXT
    raise_on_duplicate: false
- ref: list_tables
  action: core.table.list_tables
- ref: table_metadata
  action: core.table.get_table_metadata
  args:
    name: asset_inventory
```

## `core.table.get_table_metadata`

Get a table's metadata by name. This includes the columns and whether they are indexed.

### Inputs

<ParamField path="name" type="string" required>

The name of the table to get.

</ParamField>

### Examples

**Create and inspect a table**

```yaml
- ref: create_table
  action: core.table.create_table
  args:
    name: asset_inventory
    columns:
      - name: hostname
        type: TEXT
      - name: owner
        type: TEXT
    raise_on_duplicate: false
- ref: list_tables
  action: core.table.list_tables
- ref: table_metadata
  action: core.table.get_table_metadata
  args:
    name: asset_inventory
```

## `core.table.lookup`

Get a single row from a table corresponding to the given column and value.

### Inputs

<ParamField path="column" type="string" required>

The column to lookup the value in.

</ParamField>

<ParamField path="table" type="string" required>

The table to lookup the value in.

</ParamField>

<ParamField path="value" type="any" required>

The value to lookup.

</ParamField>

### Examples

**Look up rows**

```yaml
- ref: lookup_row
  action: core.table.lookup
  args:
    table: asset_inventory
    column: hostname
    value: ${{ TRIGGER.hostname }}
- ref: row_exists
  action: core.table.is_in
  args:
    table: asset_inventory
    column: hostname
    value: ${{ TRIGGER.hostname }}
- ref: lookup_many_rows
  action: core.table.lookup_many
  args:
    table: asset_inventory
    column: owner
    value: secops
    limit: 25
```

## `core.table.is_in`

Check if a value exists in a table column.

### Inputs

<ParamField path="column" type="string" required>

The column to check in.

</ParamField>

<ParamField path="table" type="string" required>

The table to check.

</ParamField>

<ParamField path="value" type="any" required>

The value to check for.

</ParamField>

### Examples

**Look up rows**

```yaml
- ref: lookup_row
  action: core.table.lookup
  args:
    table: asset_inventory
    column: hostname
    value: ${{ TRIGGER.hostname }}
- ref: row_exists
  action: core.table.is_in
  args:
    table: asset_inventory
    column: hostname
    value: ${{ TRIGGER.hostname }}
- ref: lookup_many_rows
  action: core.table.lookup_many
  args:
    table: asset_inventory
    column: owner
    value: secops
    limit: 25
```

## `core.table.lookup_many`

Get multiple rows from a table corresponding to the given column and values.

### Inputs

<ParamField path="column" type="string" required>

The column to lookup the value in.

</ParamField>

<ParamField path="table" type="string" required>

The table to lookup the value in.

</ParamField>

<ParamField path="value" type="any" required>

The value to lookup.

</ParamField>

<ParamField path="limit" type="integer">

The maximum number of rows to return.

Default: `100`.

</ParamField>

### Examples

**Look up rows**

```yaml
- ref: lookup_row
  action: core.table.lookup
  args:
    table: asset_inventory
    column: hostname
    value: ${{ TRIGGER.hostname }}
- ref: row_exists
  action: core.table.is_in
  args:
    table: asset_inventory
    column: hostname
    value: ${{ TRIGGER.hostname }}
- ref: lookup_many_rows
  action: core.table.lookup_many
  args:
    table: asset_inventory
    column: owner
    value: secops
    limit: 25
```

## `core.table.search_rows`

Search for rows in a table with optional filtering.

### Inputs

<ParamField path="table" type="string" required>

The table to search in.

</ParamField>

<ParamField path="cursor" type="string | null">

Cursor for pagination.

Default: `null`.

</ParamField>

<ParamField path="end_time" type="string | null">

Filter rows created before this time.

Default: `null`.

</ParamField>

<ParamField path="limit" type="integer">

The maximum number of rows to return.

Default: `100`.

</ParamField>

<ParamField path="paginate" type="boolean">

If true, return cursor pagination metadata along with items.

Default: `false`.

</ParamField>

<ParamField path="reverse" type="boolean">

Reverse pagination direction.

Default: `false`.

</ParamField>

<ParamField path="search_term" type="string | null">

Text to search for across all text and JSONB columns.

Default: `null`.

</ParamField>

<ParamField path="start_time" type="string | null">

Filter rows created after this time.

Default: `null`.

</ParamField>

<ParamField path="updated_after" type="string | null">

Filter rows updated after this time.

Default: `null`.

</ParamField>

<ParamField path="updated_before" type="string | null">

Filter rows updated before this time.

Default: `null`.

</ParamField>

### Examples

**Search table rows**

```yaml
- ref: search_rows
  action: core.table.search_rows
  args:
    table: asset_inventory
    search_term: database
    limit: 50
    paginate: true
```

## `core.table.insert_row`

Insert a row into a table.

### Inputs

<ParamField path="row_data" type="object" required>

The data to insert into the row.

</ParamField>

<ParamField path="table" type="string" required>

The table to insert the row into.

</ParamField>

<ParamField path="upsert" type="boolean">

If true, update the row if it already exists (based on primary key).

Default: `false`.

</ParamField>

### Examples

**Insert, update, and delete rows**

```yaml
- ref: insert_row
  action: core.table.insert_row
  args:
    table: asset_inventory
    row_data:
      hostname: app-01
      owner: secops
- ref: insert_rows
  action: core.table.insert_rows
  args:
    table: asset_inventory
    rows_data:
      - hostname: app-02
        owner: secops
      - hostname: app-03
        owner: platform
- ref: update_row
  action: core.table.update_row
  args:
    table: asset_inventory
    row_id: ${{ TRIGGER.row_id }}
    row_data:
      owner: incident-response
- ref: delete_row
  action: core.table.delete_row
  args:
    table: asset_inventory
    row_id: ${{ TRIGGER.row_id }}
```

## `core.table.insert_rows`

Insert multiple rows into a table.

### Inputs

<ParamField path="rows_data" type="array[object]" required>

The list of data to insert into the table.

</ParamField>

<ParamField path="table" type="string" required>

The table to insert the rows into.

</ParamField>

<ParamField path="upsert" type="boolean">

If true, update the rows if they already exist (based on primary key).

Default: `false`.

</ParamField>

### Examples

**Insert, update, and delete rows**

```yaml
- ref: insert_row
  action: core.table.insert_row
  args:
    table: asset_inventory
    row_data:
      hostname: app-01
      owner: secops
- ref: insert_rows
  action: core.table.insert_rows
  args:
    table: asset_inventory
    rows_data:
      - hostname: app-02
        owner: secops
      - hostname: app-03
        owner: platform
- ref: update_row
  action: core.table.update_row
  args:
    table: asset_inventory
    row_id: ${{ TRIGGER.row_id }}
    row_data:
      owner: incident-response
- ref: delete_row
  action: core.table.delete_row
  args:
    table: asset_inventory
    row_id: ${{ TRIGGER.row_id }}
```

## `core.table.update_row`

Update a row in a table.

### Inputs

<ParamField path="row_data" type="object" required>

The new data for the row.

</ParamField>

<ParamField path="row_id" type="string" required>

The ID of the row to update.

</ParamField>

<ParamField path="table" type="string" required>

The table to update the row in.

</ParamField>

### Examples

**Insert, update, and delete rows**

```yaml
- ref: insert_row
  action: core.table.insert_row
  args:
    table: asset_inventory
    row_data:
      hostname: app-01
      owner: secops
- ref: insert_rows
  action: core.table.insert_rows
  args:
    table: asset_inventory
    rows_data:
      - hostname: app-02
        owner: secops
      - hostname: app-03
        owner: platform
- ref: update_row
  action: core.table.update_row
  args:
    table: asset_inventory
    row_id: ${{ TRIGGER.row_id }}
    row_data:
      owner: incident-response
- ref: delete_row
  action: core.table.delete_row
  args:
    table: asset_inventory
    row_id: ${{ TRIGGER.row_id }}
```

## `core.table.delete_row`

Delete a row from a table.

### Inputs

<ParamField path="row_id" type="string" required>

The ID of the row to delete.

</ParamField>

<ParamField path="table" type="string" required>

The table to delete the row from.

</ParamField>

### Examples

**Insert, update, and delete rows**

```yaml
- ref: insert_row
  action: core.table.insert_row
  args:
    table: asset_inventory
    row_data:
      hostname: app-01
      owner: secops
- ref: insert_rows
  action: core.table.insert_rows
  args:
    table: asset_inventory
    rows_data:
      - hostname: app-02
        owner: secops
      - hostname: app-03
        owner: platform
- ref: update_row
  action: core.table.update_row
  args:
    table: asset_inventory
    row_id: ${{ TRIGGER.row_id }}
    row_data:
      owner: incident-response
- ref: delete_row
  action: core.table.delete_row
  args:
    table: asset_inventory
    row_id: ${{ TRIGGER.row_id }}
```

## `core.table.download`

Download a table's data by name as list of dicts, JSON string, NDJSON string, CSV or Markdown.

### Inputs

<ParamField path="name" type="string" required>

The name of the table to download.

</ParamField>

<ParamField path="format" type="string | null">

The format to download the table data in.

Default: `null`.

</ParamField>

<ParamField path="limit" type="integer">

The maximum number of rows to download.

Default: `1000`.

</ParamField>

### Examples

**Export table data**

```yaml
- ref: export_table
  action: core.table.download
  args:
    name: asset_inventory
    format: csv
    limit: 1000
```
