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

# Export data to MATLAB

> Get telemetry data from Sift into MATLAB for analysis using the official Python client or the REST API.

export const MintTable = ({columns = [], rows = [], columnWidths = []}) => {
  const pushTextWithLineBreaks = (parts, text, keyBase) => {
    const segments = String(text).split(/\\n|\n/);
    segments.forEach((segment, idx) => {
      if (segment) {
        parts.push(<span key={`${keyBase}-text-${idx}`}>{segment}</span>);
      }
      if (idx < segments.length - 1) {
        parts.push(<br key={`${keyBase}-br-${idx}`} />);
      }
    });
  };
  const parseMarkdown = text => {
    if (text === null || text === undefined) return "";
    const str = String(text);
    const parts = [];
    let lastIndex = 0;
    const pattern = /(`[^`]+`|\*\*[^*]+\*\*|\*[^*]+\*|\[([^\]]+)\]\(([^)]+)\))/g;
    let match;
    while (true) {
      match = pattern.exec(str);
      if (match === null) {
        break;
      }
      if (match.index > lastIndex) {
        pushTextWithLineBreaks(parts, str.substring(lastIndex, match.index), `before-${lastIndex}`);
      }
      const fullMatch = match[0];
      if (fullMatch.startsWith("`") && fullMatch.endsWith("`")) {
        parts.push(<code key={match.index}>{fullMatch.slice(1, -1)}</code>);
      } else if (fullMatch.startsWith("**") && fullMatch.endsWith("**")) {
        parts.push(<strong key={match.index}>{fullMatch.slice(2, -2)}</strong>);
      } else if (fullMatch.startsWith("*") && fullMatch.endsWith("*")) {
        parts.push(<em key={match.index}>{fullMatch.slice(1, -1)}</em>);
      } else if (fullMatch.startsWith("[")) {
        const linkText = match[2];
        const linkUrl = match[3];
        parts.push(<a key={match.index} href={linkUrl} className="text-black-600 dark:text-black-400">
            {linkText}
          </a>);
      }
      lastIndex = pattern.lastIndex;
    }
    if (lastIndex < str.length) {
      pushTextWithLineBreaks(parts, str.substring(lastIndex), `tail-${lastIndex}`);
    }
    if (parts.length > 0) {
      return parts;
    }
    const plainParts = [];
    pushTextWithLineBreaks(plainParts, str, "plain");
    return plainParts.length ? plainParts : str;
  };
  const safeColumns = Array.isArray(columns) ? columns : [];
  const safeRows = Array.isArray(rows) ? rows : [];
  const safeColumnWidths = Array.isArray(columnWidths) ? columnWidths : [];
  const hasColumnWidths = safeColumnWidths.some(w => w !== null && w !== undefined && w !== "");
  const toCssWidth = width => typeof width === "number" ? `${width}px` : String(width);
  const getColumnStyle = idx => {
    const rawWidth = safeColumnWidths[idx];
    if (rawWidth === null || rawWidth === undefined || rawWidth === "") {
      return undefined;
    }
    const width = toCssWidth(rawWidth);
    return {
      width,
      minWidth: width
    };
  };
  const containerStyle = hasColumnWidths ? undefined : {
    overflowX: "auto"
  };
  const tableStyle = hasColumnWidths ? {
    tableLayout: "fixed",
    width: "100%"
  } : {
    width: "max-content",
    minWidth: "100%"
  };
  if (!Array.isArray(columns) || !Array.isArray(rows) || !Array.isArray(columnWidths)) {
    console.warn("MintTable received invalid props:", {
      columns,
      rows,
      columnWidths
    });
  }
  if (!safeColumns.length && !safeRows.length) {
    return null;
  }
  return <div className="mint-table-container" style={containerStyle}>
      <table style={tableStyle}>
        {hasColumnWidths && <colgroup>
            {safeColumns.map((_, idx) => {
    const style = getColumnStyle(idx);
    return <col key={idx} style={style} />;
  })}
          </colgroup>}
        <thead>
          <tr>
            {safeColumns.map((col, idx) => <th key={idx} className="text-left" style={getColumnStyle(idx)}>
                <b>{parseMarkdown(col)}</b>
              </th>)}
          </tr>
        </thead>
        <tbody>
          {safeRows.map((row, rIdx) => {
    const safeRow = Array.isArray(row) ? row : [];
    return <tr key={rIdx}>
                {safeRow.map((cell, cIdx) => <td key={cIdx} style={getColumnStyle(cIdx)}>
                    {parseMarkdown(cell)}
                  </td>)}
              </tr>;
  })}
        </tbody>
      </table>
    </div>;
};

After data is stored in Sift, it can be exported into MATLAB for analysis. Sift recommends using the official Python [client](https://pypi.org/project/sift-stack-py/) via MATLAB's built-in Python interface. If your team cannot use Python, the REST API is also supported natively from MATLAB.

## Before you begin

To export data from Sift to MATLAB programmatically, make sure you have the following:

* [A Sift API key and your base URLs](/documentation/manage/set-up-api-access).
* The ID of the Run and the IDs of the Channels you want to export. You can find these in the Sift UI or by querying the [Runs](/api-reference/runservice/listruns) and [Channels](/api-reference/channelservice/listchannels) endpoints.

## How exporting to MATLAB works

Sift provides two programmatic export mechanisms: **data querying**, which returns Channel data immediately in the response, and **export data to file**, which processes the export in the background and produces a downloadable file. Both mechanisms are available through the two paths that allow you to import data into MATLAB.

<MintTable
  columns={['', '[Path 1: MATLAB to Python bindings](#path-1-matlab-to-python-bindings)', '[Path 2: REST API via webread](#path-2-rest-api-via-webread)']}
  columnWidths={['20%', '40%', '40%']}
  rows={[
['**What it does**', 'Uses Sift\'s official Python client via MATLAB\'s built-in Python interface', 'Calls the Sift REST API directly using MATLAB\'s native `webread` and `webwrite` functions'],
['**Best for**', 'Teams that have Python available and want to use the full Sift Python client feature set', 'Teams that cannot or prefer not to use Python'],
['**Requirements**', 'Python 3.8 or later, `sift-stack-py`, and `pyarrow` installed on the same machine as MATLAB', 'MATLAB only, no additional dependencies'],
]}
/>

## Path 1: MATLAB to Python bindings

Path 1 uses MATLAB's built-in Python interface to call Sift's official Python client (`sift-stack-py`) directly from your MATLAB environment. The `py.` prefix on any MATLAB command is what invokes that interface.

<MintTable
  columns={['Mechanism', 'Description']}
  columnWidths={['35%', '65%']}
  rows={[
['[Mechanism 1: Data querying](#mechanism-1-data-querying)', 'Query Channel data over a time window and load it into a MATLAB timetable'],
['[Mechanism 2: Export data to file](#mechanism-2-export-data-to-file)', 'Submit a bulk export job and download the result as a file. \n\n Supported formats: CSV, Parquet, and Sun/WinPlot (Sun/WinPlot is feature-flagged for certain gov cloud customers only)'],
]}
/>

### Set up the Python environment

Path 1 requires Python 3.8 or later installed on the same machine as MATLAB. Both mechanisms in Path 1 rely on the Sift Python client running inside a Python virtual environment that MATLAB can access.

Complete the following steps once before running either mechanism. This sets up the Python environment, points MATLAB to it, and stores your Sift credentials.

<Steps>
  <Step title="Create a virtual environment">
    Run the following in your terminal:

    ```bash theme={null}
    python3 -m venv sift-matlab-env
    source sift-matlab-env/bin/activate
    ```
  </Step>

  <Step title="Install the required packages">
    With the virtual environment active, install the Sift client and its dependencies:

    ```bash theme={null}
    pip install sift-stack-py pyarrow
    ```
  </Step>

  <Step title="Point MATLAB to your virtual environment">
    In the MATLAB Command Window, run:

    ```matlab theme={null}
    pyenv(Version="/path/to/sift-matlab-env/bin/python")
    ```

    Replace `/path/to/sift-matlab-env` with the full path to the virtual environment you created. To find it, run `which python` in your terminal while the virtual environment is active.

    You only need to do this setup once. MATLAB will remember the Python environment across sessions.
  </Step>

  <Step title="Verify your Python environment">
    Confirm MATLAB is pointing at the correct Python environment:

    ```matlab theme={null}
    pyenv
    ```

    If the wrong environment is selected, point MATLAB at the correct one:

    ```matlab theme={null}
    pyenv(Version="/path/to/your/python")
    ```
  </Step>

  <Step title="Set your Sift credentials">
    Create a `.env` file in a text editor and save it to MATLAB's current working directory. Run `pwd` in the MATLAB Command Window to find that location. Add the following to the file:

    ```
    SIFT_API_KEY=your-api-key-here
    SIFT_GRPC_URI=your-grpc-url-here
    SIFT_REST_URI=your-rest-url-here
    ```

    Then load the credentials in the MATLAB Command Window:

    ```matlab theme={null}
    loadenv('.env')
    disp(getenv('SIFT_API_KEY'))  % confirms the key was loaded
    ```
  </Step>
</Steps>

### Mechanism 1: Data querying

<Steps>
  <Step title="Initialize the client">
    Run the following in the MATLAB Command Window to connect to Sift:

    ```matlab theme={null}
    sift_mod = py.importlib.import_module('sift_client');
    client = sift_mod.SiftClient(pyargs( ...
        'api_key',  getenv('SIFT_API_KEY'), ...
        'grpc_url', getenv('SIFT_GRPC_URI'), ...
        'rest_url', getenv('SIFT_REST_URI')));

    disp(client)  % confirms the client connected
    ```
  </Step>

  <Step title="List Channels and query data">
    Use [client.runs.find](https://sift-stack.github.io/sift/python/latest/reference/sift_client/resources/#sift_client.resources.RunsAPI.find) to find a single Run by name, or [client.runs.list\_](https://sift-stack.github.io/sift/python/latest/reference/sift_client/resources/#sift_client.resources.RunsAPI.list_) to search across multiple Runs. When a Run is provided to [client.channels.get\_data\_as\_arrow](https://sift-stack.github.io/sift/python/latest/reference/sift_client/resources/#sift_client.resources.ChannelsAPI.get_data_as_arrow), the Run's time range is used automatically so `start_time` and `end_time` are not needed.

    Find the Run. Use Option 1 to find a single Run by name:

    ```matlab theme={null}
    run = client.runs.find(pyargs('name', 'YOUR_RUN_NAME'));
    disp(run)  % shows the Run name, ID and time range
    ```

    Use Option 2 to search across multiple Runs:

    ```matlab theme={null}
    runs = client.runs.list_(pyargs('name', 'YOUR_RUN_NAME'));
    run = runs{1};
    disp(run)  % shows the Run name, ID and time range
    ```

    Find Channels scoped to the Run using [client.channels.list\_](https://sift-stack.github.io/sift/python/latest/reference/sift_client/resources/#sift_client.resources.ChannelsAPI.list_):

    ```matlab theme={null}
    channels = client.channels.list_(pyargs( ...
        'run',  run, ...
        'name', 'YOUR_CHANNEL_NAME'));

    disp(channels)  % shows the Channel objects found
    ```

    If you already know the Channel ID, you can use [client.channels.get](https://sift-stack.github.io/sift/python/latest/reference/sift_client/resources/#sift_client.resources.ChannelsAPI.get) instead:

    ```matlab theme={null}
    channel = client.channels.get(pyargs('channel_id', 'YOUR_CHANNEL_ID'));
    channels = py.list({channel});
    disp(channels)  % confirms the Channel was found
    ```

    Query data for the full Run:

    ```matlab theme={null}
    arrow_result = client.channels.get_data_as_arrow(pyargs( ...
        'channels', channels, ...
        'run',      run));

    disp(arrow_result)  % shows the Python dict with Channel names and Arrow tables
    ```

    To query a specific time window within the Run, pass `start_time` and `end_time`:

    ```matlab theme={null}
    utc = py.datetime.timezone(py.datetime.timedelta(0));

    start_dt = py.datetime.datetime(int32(YYYY), int32(MM), int32(DD), int32(HH), int32(MM), int32(SS), pyargs('tzinfo', utc));  % e.g. int32(2026), int32(4), int32(15), int32(0), int32(17), int32(49)
    end_dt   = py.datetime.datetime(int32(YYYY), int32(MM), int32(DD), int32(HH), int32(MM), int32(SS), pyargs('tzinfo', utc));  % e.g. int32(2026), int32(4), int32(15), int32(0), int32(18), int32(26)

    arrow_result = client.channels.get_data_as_arrow(pyargs( ...
        'channels',   channels, ...
        'run',        run, ...
        'start_time', start_dt, ...
        'end_time',   end_dt));

    disp(arrow_result)  % shows the Python dict with Channel names and Arrow tables
    ```

    `arrow_result` is a Python dict where each key is a Channel name and the value is an Apache Arrow table.
  </Step>

  <Step title="Write to Parquet and load into MATLAB">
    Data returned by [get\_data\_as\_arrow](https://sift-stack.github.io/sift/python/latest/reference/sift_client/resources/#sift_client.resources.ChannelsAPI.get_data_as_arrow) comes back as a Python object that MATLAB cannot consume directly. Writing it to a Parquet file and loading it with `parquetread` converts it into a format MATLAB understands.

    Replace `YOUR_CHANNEL_NAME` with the exact name of your Channel as it appears in Sift, for example `temperature`.

    ```matlab theme={null}
    pq = py.importlib.import_module('pyarrow.parquet');
    pq.write_table(arrow_result{'YOUR_CHANNEL_NAME'}, 'sift_export.parquet');

    tt = parquetread('sift_export.parquet', ...
        'OutputType', 'timetable', ...
        'RowTimes',   'x__index_level_0__');

    % Rename the time dimension to something readable
    tt.Properties.DimensionNames{1} = 'time';

    disp(tt)
    ```

    The output will look similar to this:

    ```
                time            temperature
        ____________________    ___________

        15-Apr-2026 00:17:49      25.152
        15-Apr-2026 00:17:50      29.854
        15-Apr-2026 00:17:50      28.995
        15-Apr-2026 00:17:51      31.947
        15-Apr-2026 00:17:51      23.975
    ```

    You will see a warning about table variable names being modified. This is expected. MATLAB automatically renames the internal timestamp column from `__index_level_0__` to `x__index_level_0__` to comply with MATLAB identifier rules. The timetable is created correctly and the warning can be safely ignored.
  </Step>
</Steps>

### Mechanism 2: Export data to file

<Steps>
  <Step title="Submit the export job">
    Submit the job using [client.data\_export.export](https://sift-stack.github.io/sift/python/latest/reference/sift_client/resources/#sift_client.resources.DataExportAPI.export). The `output_format` parameter accepts [ExportOutputFormat](https://sift-stack.github.io/sift/python/latest/reference/sift_client/sift_types/export/#sift_client.sift_types.export.ExportOutputFormat) values including `CSV`, `PARQUET`, and `SUN`.

    ```matlab theme={null}
    export_mod = py.importlib.import_module('sift_client.sift_types.export');
    ExportOutputFormat = export_mod.ExportOutputFormat;

    job = client.data_export.export(pyargs( ...
        'output_format', ExportOutputFormat.CSV, ...
        'runs',          py.list({'YOUR_RUN_ID'}), ...
        'channels',      py.list({'YOUR_CHANNEL_ID'})));

    % Extract the job ID from the Python job object
    jobId = char(job.id_);
    disp(jobId)  % confirms the job was submitted
    ```
  </Step>

  <Step title="Wait for the job and download the result">
    Use `client.jobs.wait_and_download` with `show_progress` set to `false` to avoid the progress bar conflict with MATLAB's Command Window:

    ```matlab theme={null}
    client.jobs.wait_and_download(pyargs( ...
        'job',           job, ...
        'show_progress', false, ...
        'output_dir',    '.'));
    ```
  </Step>

  <Step title="Load the result into MATLAB">
    The exported file is saved to MATLAB's current working directory. Run `dir` to find the filename:

    ```matlab theme={null}
    dir('*.csv')
    ```

    Then load it:

    ```matlab theme={null}
    T = readtable('YOUR_FILENAME.csv', ...
        'Delimiter',          ',', ...
        'VariableNamingRule', 'preserve');
    disp(T)
    ```

    The column names include the full Run and Asset path, for example `runName|assetName|temperature`. To rename a column after loading:

    ```matlab theme={null}
    T.Properties.VariableNames{2} = 'temperature';
    ```
  </Step>
</Steps>

## Path 2: REST API via webread

Path 2 uses MATLAB's native `webread` and `webwrite` functions to call the Sift REST API directly. No Python installation is required.

<MintTable
  columns={['Mechanism', 'Description']}
  columnWidths={['35%', '65%']}
  rows={[
['[Mechanism 1: Data querying](#mechanism-1-data-querying-1)', 'Query Channel data over a time window and load it into a MATLAB timetable'],
['[Mechanism 2: Export data to file](#mechanism-2-export-data-to-file-1)', 'Submit a bulk export job and download the result as a file. \n\n Supported formats: CSV, Parquet, and Sun/WinPlot (Sun/WinPlot is feature-flagged for certain gov cloud customers only)'],
]}
/>

### Mechanism 1: Data querying

<Steps>
  <Step title="Set your Sift credentials">
    If you already completed the Path 1 setup, your `.env` file already contains the required credentials. Run the following to load them:

    ```matlab theme={null}
    loadenv('.env')
    ```

    If you are starting with Path 2 directly, create a `.env` file in a text editor and save it to MATLAB's current working directory. Run `pwd` in the MATLAB Command Window to find that location. Add the following to the file:

    ```
    SIFT_API_KEY=your-api-key-here
    SIFT_REST_URI=your-rest-url-here
    ```

    Then load the credentials:

    ```matlab theme={null}
    loadenv('.env')
    ```
  </Step>

  <Step title="Query Channel data">
    The examples below use `webwrite` to call [POST /api/v2/data](/api-reference/dataservice/getdata-1) (`GetData`). See the [GetData endpoint reference](/api-reference/dataservice/getdata-1) for the full schema.

    ```matlab theme={null}
    opts = weboptions( ...
        'RequestMethod', 'post', ...
        'MediaType',     'application/json', ...
        'ContentType',   'json', ...
        'HeaderFields',  {'Authorization', ['Bearer ' getenv('SIFT_API_KEY')]});

    query = struct( ...
        'queries', {{struct('channel', struct( ...
            'channelId', 'YOUR_CHANNEL_ID', ...
            'runId',     'YOUR_RUN_ID'))}}, ...
        'startTime', 'YYYY-MM-DDTHH:MM:SS.sssZ', ...  % e.g. 2026-04-15T00:17:49.984Z
        'endTime',   'YYYY-MM-DDTHH:MM:SS.sssZ', ...  % e.g. 2026-04-15T00:18:26.609Z
        'pageSize',  100000, ...
        'pageToken', '');

    result = webwrite([getenv('SIFT_REST_URI') '/api/v2/data'], query, opts);
    disp(result)  % shows the raw API response
    ```

    If `result.nextPageToken` is non-empty, resend the request with `pageToken` set to that value to retrieve the next page.
  </Step>

  <Step title="Load into a MATLAB timetable">
    ```matlab theme={null}
    dt = struct2table(result.data(1).values);
    dt.time = datetime(dt.timestamp, ...
        'InputFormat', 'uuuu-MM-dd''T''HH:mm:ss.SSS''Z''', ...
        'TimeZone',    'utc');
    dt.timestamp = [];
    tt = table2timetable(dt, 'RowTimes', 'time');

    disp(tt)
    ```
  </Step>
</Steps>

### Mechanism 2: Export data to file

<Steps>
  <Step title="Submit the export job">
    The examples below use `webwrite` to call [POST /api/v1/export](/api-reference/exportservice/exportdata) (`ExportData`) and [GET /api/v1/export/{jobId}/download-url](/api-reference/exportservice/getdownloadurl) (`GetDownloadUrl`). See the [ExportData endpoint reference](/api-reference/exportservice/exportdata) and the [GetDownloadUrl endpoint reference](/api-reference/exportservice/getdownloadurl) for the full schema.

    ```matlab theme={null}
    opts = weboptions( ...
        'RequestMethod', 'post', ...
        'MediaType',     'application/json', ...
        'ContentType',   'json', ...
        'HeaderFields',  {'Authorization', ['Bearer ' getenv('SIFT_API_KEY')]});

    exportBody = struct( ...
        'runsAndTimeRange', struct('runIds', {{'YOUR_RUN_ID'}}), ...
        'channelIds',       {{'YOUR_CHANNEL_ID'}}, ...
        'outputFormat',     'EXPORT_OUTPUT_FORMAT_CSV');

    exportResult = webwrite([getenv('SIFT_REST_URI') '/api/v1/export'], exportBody, opts);
    disp(exportResult)  % shows the job ID and status
    ```
  </Step>

  <Step title="Retrieve the download link">
    ```matlab theme={null}
    if ~isempty(exportResult.presignedUrl)
        websave('sift_export.zip', exportResult.presignedUrl);
    else
        getOpts = weboptions( ...
            'HeaderFields', {'Authorization', ['Bearer ' getenv('SIFT_API_KEY')]}, ...
            'ContentType', 'json');
        jobId = exportResult.jobId;
        presignedUrl = '';
        while isempty(presignedUrl)
            pause(5);
            urlResult    = webread([getenv('SIFT_REST_URI') '/api/v1/export/' jobId '/download-url'], getOpts);
            presignedUrl = string(urlResult.presignedUrl);
        end
        websave('sift_export.zip', presignedUrl);
    end
    ```
  </Step>

  <Step title="Unzip and load into MATLAB">
    ```matlab theme={null}
    unzip('sift_export.zip', 'sift_export');
    ```

    The ZIP contains a file with a name generated by Sift. The format matches the `outputFormat` you specified when submitting the job, for example `sift_data_export_2026-05-19_172841.csv` for CSV. Run the following to see the exact filename:

    ```matlab theme={null}
    dir('sift_export')
    ```

    Then load it using the actual filename:

    ```matlab theme={null}
    T = readtable('sift_export/YOUR_FILENAME.csv', ...
        'Delimiter',         ',', ...
        'VariableNamingRule', 'preserve');
    disp(T)
    ```

    The column names include the full run and asset path, for example `runName|assetName|temperature`. To rename a column after loading:

    ```matlab theme={null}
    T.Properties.VariableNames{2} = 'temperature';
    ```
  </Step>
</Steps>

## Verify

Once data is loaded into MATLAB, confirm it looks correct. Use `tt` for timetables (Mechanism 1) and `T` for tables (Mechanism 2).

* Check the data is not empty:

  ```matlab theme={null}
  size(tt)   % for timetables
  size(T)    % for tables
  ```

* Check the column names match your expected Channel names:

  ```matlab theme={null}
  tt.Properties.VariableNames   % for timetables
  T.Properties.VariableNames    % for tables
  ```

* Check the timestamp range covers what you requested:

  ```matlab theme={null}
  tt.Properties.RowTimes([1 end])   % for timetables
  T{[1 end], 1}                     % for tables
  ```

* Preview the first few rows:

  ```matlab theme={null}
  head(tt)   % for timetables
  head(T)    % for tables
  ```

## Reference

* [Export data programmatically](/api/export/export-data-programmatically)
