> ## 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.

# Assets settings

> Settings, options, and behaviors for Assets in Sift.

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>;
};

Assets in Sift represent physical or virtual entities that generate time-series data through their Channels. They can represent a wide range of systems, from tangible objects such as vehicles and hardware testbeds to intangible systems like simulators or continuous integration (CI) pipelines. Assets are the sources of telemetry data, meaning any entities that produce structured measurements over time.

## Asset modeling

Asset modeling in Sift is about deciding which physical or virtual systems should be represented as Assets and how to structure them to reflect your test setup, telemetry flow, or workflow. Good modeling helps ensure that telemetry is organized clearly, supports analysis features like Rules and Runs, and enables long-term reuse and discoverability.

The following table summarizes the two most common Asset modeling strategies in Sift.

<MintTable
  columns={['Workflow', 'Modeling strategy', 'Example']}
  columnWidths={['20%', '45%', '35%']}
  rows={[
['Hardware testing', 'Create separate Assets for the test cell and each test article that produce telemetry. Use a Run to capture test execution involving those Assets. If only one component produces data, it can be the sole Asset, and the Run used to record test context. If only the test cell produces data, it can be the sole Asset, with the test article described in the Run. Include serial numbers or other unique identifiers for physical articles.', 'The Asset thermal chamber represents the test cell, and vehicle 542 represents the test article. Both are included in a single Run. In cases where only the vehicle logs data, it may be the only Asset.'],
['CI or simulation workflows', 'Reuse a single Asset to represent the simulation or CI job. Avoid creating a new Asset for each job instance. Use Runs to track each build or simulation execution.', 'The Asset sim runner is reused across multiple pull requests. Each CI build is represented by a separate Run linked to that same Asset.'],
]}
/>

## Naming conventions

When naming an Asset, aim to make the name clear enough that anyone can understand three key things just by looking at it: its **type**, a unique **identifier**, and a **modifier** that describes its environment or context. A common and recommended convention is to separate these parts with underscores for readability.

```
type_identifier_modifier
```

<MintTable
  columns={['Token', 'Description', 'Example']}
  columnWidths={['20%', '50%', '30%']}
  rows={[
['type', 'What kind of system, hardware, or software is this Asset?', 'capsule, rover, thermal-chamber'],
['identifier', 'What uniquely distinguishes this Asset from others of the same type?', 'sn542, slc40, jdoe-linux-vm'],
['modifier', 'What environment or context is this Asset used in?', 'sim, hitl, hootl'],
]}
/>

<MintTable
  columns={['Example name', 'Type', 'Identifier', 'Modifier', 'Description']}
  columnWidths={['25%', '10%', '15%', '10%', '40%']}
  rows={[
['capsule_sn542_sim', 'capsule', 'sn542', 'sim', 'This Asset name tells us it is a capsule (type), specifically unit sn542 (identifier), running in a simulation environment (modifier).'],
['rover_slc40_hitl', 'rover', 'slc40', 'hitl', 'This Asset name tells us it is a rover (type), located at Space Launch Complex 40 (identifier), being tested in a hardware-in-the-loop environment (modifier).'],
['thermal-chamber_jdoe-linux-vm_hootl', 'thermal-chamber', 'jdoe-linux-vm', 'hootl', 'This Asset name tells us it is a thermal chamber (type), identified by the hostname jdoe-linux-vm (identifier), and part of a hardware-out-of-the-loop test setup (modifier).'],
]}
/>

Additional naming guidelines:

* Do not include special characters in Asset names.
* Use hyphens to separate words or fill spaces within token names.
* Use lowercase letters in Asset names. This is not required, since Asset names are case insensitive, but it improves consistency and readability. The stored display name retains the casing from the first ingest.

## Create an asset

Creating an Asset requires the **Create asset** [permission](/documentation/reference/manage/authorization-models-settings#role-permissions).

Assets can be created in the following ways:

* **Automatically during ingestion.** Uploading a file, creating a Run, or setting up an ingestion config creates the named Asset if it does not already exist.
* **Explicitly, before any data exists.** Select **New Asset** on the Assets page, or call `CreateAsset` on the [Asset API](/api/reference/protocol-buffers/assets). Creating Assets in advance lets you apply your [naming conventions](#naming-conventions) during planning instead of deciding names at the moment data lands.

Selecting **New Asset** opens a modal where you enter the Asset name and optionally add tags and metadata. Sift validates the name and returns an error if an Asset with that name already exists. On success, Sift opens the new Asset's overview page.

An Asset created this way is empty until data arrives. Streamed data and file uploads that reference the Asset name attach to the existing Asset rather than creating a duplicate.

## Rule evaluation scope

Rules in Sift are evaluated within the scope of a single Asset and its Channels. Any telemetry you want to analyze together using Rules must be grouped under the same Asset. It is acceptable for multiple clients or data sources to stream to the same Asset simultaneously, as long as they represent parts of the same system or workflow.

## Ingestion monitoring

**sift\_app** is a built-in system Asset that provides real-time telemetry about your data pipeline's performance and stability. It surfaces internal metrics through the same infrastructure you use for your own data, allowing you to build dashboards and set up alerts for specific Channels using familiar tools.

The **sift\_app** Asset is organized into four subsections. Each subsection represents a different layer of the ingestion journey, moving from your local client to Sift's internal processing, error handling, and file-based data imports.

```bash theme={null}
sift_app
├── data_import
├── dlq_ingestion
├── ingest_grpc
└── stream
```

### data\_import

The **data\_import** subsection provides telemetry for data imported into Sift via file upload, such as CSV, Parquet, Ch10, or TDMS files. Metrics are emitted when a data import Job completes (successfully or with an error).

```bash theme={null}
sift_app
├── data_import
    └── <customer_asset_name>
        └── processor
            └── [Various Channels]
```

<Note>
  **Applicability**: These metrics apply only to data imported via file upload (for example, through the UI, upload APIs, or Jobs that process uploaded files), and not to data ingested through the gRPC stream APIs.
</Note>

<MintTable
  columns={['Channel', 'Description']}
  columnWidths={['35%', '65%']}
  rows={[
['`import_kind`', 'File type (for example: CSV, Parquet, Ch10, TDMS)'],
['`error_flag`', '1.0 if the import failed, 0.0 if successful'],
['`run_id`', 'Run ID associated with the imported data (empty if no Run)'],
['`asset_id`', 'Asset the data was imported into'],
['`job_id`', 'Data import Job ID'],
['`report_id`', 'Report ID from Rule evaluation, if any (empty if none)'],
['`num_points`', 'Total number of data points imported in the Job'],
['`num_channels`', 'Number of unique Channels imported'],
['`data_duration_seconds`', 'Time span of the data (earliest to latest timestamp). Zero if no timestamps were imported'],
['`import_duration_seconds`', 'Total time to process and ingest the Job'],
['`file_size_bytes`', 'Uncompressed file size in bytes (zero if unavailable)'],
['`file_name`', 'Display name of the imported file (original or generated)'],
['`source_url`', 'Source URL if the import was from a URL'],
]}
/>

<Tip>
  **Monitor data imports**: Use `error_flag` and `import_duration_seconds` to track failures and Job duration, and `file_size_bytes` to monitor import volume.
</Tip>

### dlq\_ingestion

The **dlq\_ingestion** subsection is dedicated to the Dead Letter Queue (DLQ) and is your primary resource for troubleshooting records that failed to ingest.

```bash theme={null}
sift_app
├── dlq_ingestion
    └── <customer_asset_name>
        ├── <reason_category>
            └── count
        └── observed_run_ids
```

<MintTable
  columns={['Channel', 'Description']}
  columnWidths={['35%', '65%']}
  rows={[
['`AlreadyExistsErrors`', 'Records rejected due to duplicates, such as a duplicate Channel name'],
['`NotFoundErrors`', 'Records that failed because a required resource, such as an ingestion configuration or descriptor, was missing'],
['`InvalidArgumentErrors`', 'Records rejected due to malformed data or type mismatches'],
['`Other`', 'A catch-all for errors that do not fall into the standard categories above'],
['`count`', 'Total number of items matching the reason category sent to the DLQ during the processing batch'],
['`observed_run_ids`', 'Comma-separated list of Run IDs observed in the DLQ data for that Asset during the processing batch'],
]}
/>

### ingest\_grpc

The **ingest\_grpc** subsection monitors the internal Sift components responsible for managing and observing your data streams.

```bash theme={null}
sift_app
├── ingest_grpc
    └── <customer_asset_name>
        └── stream_monitor
            └── [Various Channels]
```

<Note>
  **Applicability**: These metrics apply only to data ingested through the gRPC stream APIs (`IngestWithConfigDataStream` and `IngestArbitraryProtobufDataStream`), and not to data uploaded via the UI or upload APIs.
</Note>

<MintTable
  columns={['Channel', 'Description']}
  columnWidths={['35%', '65%']}
  rows={[
['`message_count`', 'Total number of messages processed over the heartbeat interval'],
['`message_rate_per_second`', 'Average message rate (messages/sec) over the heartbeat interval'],
['`byte_rate_per_second`', 'Average throughput (bytes/sec) over the heartbeat interval'],
['`min_message_size_bytes`', 'The smallest message size (in bytes) detected during the interval'],
['`max_message_size_bytes`', 'The largest message size (in bytes) detected during the interval'],
['`avg_message_size_bytes`', 'The average size of individual messages processed during the interval'],
['`total_message_size_bytes`', 'The cumulative size of all message payloads processed in the interval'],
['`avg_enqueue_duration_ns`', 'Average time (nanoseconds) taken to process and enqueue a message internally'],
['`max_enqueue_duration_ns`', 'The longest duration recorded to enqueue a single message'],
['`avg_wait_duration_ns`', 'Average time spent waiting for the next message from the stream'],
['`messages_with_high_time_drift_count`', 'Count of messages with timestamps drifting from the wall-clock by >5 seconds'],
['`points_ingested`', 'Total number of individual data points processed (Sift-type ingestion)'],
['`observed_run_count`', 'Number of unique Run IDs detected during the heartbeat interval'],
['`observed_ingestion_config_count`', 'Number of unique ingestion configuration IDs observed in the interval'],
]}
/>

<Note>
  **Monitor data**: Use `total_message_size_bytes` to build ingestion volume dashboards, and `messages_with_high_time_drift_count` to monitor for ingestion anomalies.
</Note>

### stream

The **stream** subsection provides visibility into the behavior of the Rust `sift-stream` client implementation.

```bash theme={null}
sift_app
├── stream
    └── <customer_asset_name>
        └── <ingestion_config_client_key>
            └── [Various Channels]
```

<Note>
  **Getting these metrics**: These metrics are emitted by the stream client. To receive them, ensure you are using the latest versions of either the Rust crate `sift_stream` or the Python library `sift_client`.
</Note>

<MintTable
  columns={['Channel', 'Description']}
  columnWidths={['35%', '65%']}
  rows={[
['`backups`', 'Channels in this section monitor client-side data buffering to ensure data integrity during network or service interruptions'],
['`checkpoint`', 'Channels in this section track the status of data ingestion milestones within the client'],
]}
/>

## Behavior

The following table describes known constraints and behaviors to be aware of when working with Assets.

<MintTable
  columns={['Limitation', 'Description']}
  columnWidths={['25%', '75%']}
  rows={[
['Asset names', 'Asset names are case insensitive. Uniqueness is enforced on the lowercased form of the name. The stored display name retains the casing from the first ingest.'],
['Duplicate names', 'Explicitly creating an Asset with a name that already exists returns an error. During ingestion, data sent to an existing Asset name—**including variants that differ only in casing**—attaches to that Asset instead of creating a new one.'],
['Archiving', 'Archiving an Asset is a soft delete. The Asset remains in the system in a non-active state, and its name stays reserved.'],
['Rule evaluation scope', 'Rules are evaluated within the scope of a single Asset. Telemetry you want to analyze together using Rules must be grouped under the same Asset.'],
['Metadata and tags', 'Unlike Asset names, which are fixed once created, metadata and tags can be updated at any time. They are meant to complement a clear and consistent naming convention, not replace it.'],
['Multiple data sources', 'Multiple clients or data sources can stream to the same Asset simultaneously, as long as they represent parts of the same system or workflow.'],
]}
/>
