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

# Manage Run file attachments

> Attach and remove file attachments on a Run using the 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 completing this workflow, you can attach a file to a Run, remove a file from a Run, and replace an outdated file with a new version, all using the API.

## Before you begin

* You have [a Sift API key and your base URLs](/documentation/manage/set-up-api-access).
* You have the ID of the Run where you want to attach or remove a file.

## How Run file attachments work

[Attaching](#attach-a-file-to-a-run) a file to a Run uploads it and creates a remote file entry. [Removing](#remove-a-file-from-a-run) a file deletes that entry.
To [put a new version](#update-a-run-file-attachment) of a file on a Run, remove the existing version and attach the new one.

## Attach a file to a Run

`POST /api/v0/remote-files/upload` uploads a file and creates its remote file entry in a single request. This endpoint is not currently listed in the REST API reference. Call it with the file and the Run's ID.

```bash theme={null}
curl --request POST \
  --url "$SIFT_REST_URL/api/v0/remote-files/upload" \
  --header "Authorization: Bearer $SIFT_API_KEY" \
  --form "entityId=YOUR_RUN_ID" \
  --form "entityType=runs" \
  --form "file=@/path/to/your-file.png"
```

<MintTable
  columns={['Field', 'Required', 'Description']}
  columnWidths={['20%', '20%', '60%']}
  rows={[
['`entityId`', 'Yes', 'The ID of the Run to attach the file to'],
['`entityType`', 'Yes', 'Set to `runs` to attach the file to a Run'],
['`file`', 'Yes', 'The file to upload'],
['`organizationId`', 'No', 'Your environment\'s organization ID'],
['`description`', 'No', 'A description of the file'],
['`metadata`', 'No', 'JSON metadata for an image, video, or audio file'],
['`metadataValues`', 'No', 'JSON key-value metadata to attribute to the remote file'],
]}
/>

## Remove a file from a Run

`DeleteRemoteFile` requires the file's `remoteFileId`.

1. Look up the `remoteFileId` by calling [`ListRemoteFiles`](/api-reference/remotefileservice/listremotefiles) with a `filter` scoped to the Run.

   ```bash theme={null}
   curl --request GET \
     --url "$SIFT_REST_URL/api/v1/remote-files" \
     --header "Authorization: Bearer $SIFT_API_KEY" \
     --get \
     --data-urlencode 'filter=entity_id == "YOUR_RUN_ID"'
   ```

   The response's `remoteFiles` array includes each file's `remoteFileId` and `fileName`.

2. Call [`DeleteRemoteFile`](/api-reference/remotefileservice/deleteremotefile) with that `remoteFileId`.

   ```bash theme={null}
   curl --request DELETE \
     --url "$SIFT_REST_URL/api/v1/remote-files/YOUR_REMOTE_FILE_ID" \
     --header "Authorization: Bearer $SIFT_API_KEY"
   ```

## Update a Run file attachment

[`UpdateRemoteFile`](/api-reference/remotefileservice/updateremotefile) only updates a file's `description` and `metadataValues`, not its contents. To put a new version of a file on a Run:

1. [Remove the outdated file](#remove-a-file-from-a-run).
2. [Attach the new file](#attach-a-file-to-a-run) to the same Run.

## Verify

<MintTable
  columns={['Endpoint', 'Check']}
  columnWidths={['35%', '65%']}
  rows={[
['`POST /api/v0/remote-files/upload`', 'Call [ListRemoteFiles](/api-reference/remotefileservice/listremotefiles) for the Run to confirm the new file appears.'],
['[DeleteRemoteFile](/api-reference/remotefileservice/deleteremotefile)', 'Call [ListRemoteFiles](/api-reference/remotefileservice/listremotefiles) for the Run to confirm the file no longer appears.'],
]}
/>
