Skip to main content
Version: 3.9

Upload & Download Object Attachments from a Custom UI in R-COM Gateway

Example: An Asset Document Portal

This is a beginner-friendly, end-to-end tutorial on how to add file attachments to an Object from a Custom UI and download them again.

We will build an Asset Document Portal: a small Custom UI where an operator can upload an inspection report (a PDF) for a specific asset, see the list of documents already attached to that asset, and download any of them with a click.

By the end you will understand:

  • How a file physically gets stored (through a storage provider, or the gateway database).
  • How an uploaded file gets attached to an Object using the LinkAttachment workflow action.
  • How to list an Object's attachments using the GetObjectAttachments workflow action.
  • How to download a file directly from the browser.
  • Why a Custom UI does not need an auth token, and what external systems need instead.
New to attachments?

This tutorial is the hands-on companion to the concept guide Object Attachments. Read that first if you want the high-level overview.


How It Works​

An attachment involves two separate things:

  1. The file itself — the raw bytes, stored by a storage provider (or the gateway database).
  2. The link — a record that says "this file belongs to this Object."

Uploading a file only does step 1. You must then link the returned file ID to an Object to complete the attachment. Here is the full flow for our portal:

Operator's Browser  (Custom UI)
│
│ 1. Upload file
▼
POST /RCOMAPI/api/binary-storage/upload ──► returns attachmentId (the METADATA_ID)
│
│ 2. Link the file to the asset
▼
Workflow Endpoint ──► LinkAttachment (METADATA_ID + OBJECT_ID)
│
│ 3. List the asset's documents
▼
Workflow Endpoint ──► GetObjectAttachments (OBJECT_ID) ──► file metadata list
│
│ 4. Download a file
▼
GET /RCOMAPI/api/binary-storage/download/{attachmentId} ──► file stream

Steps 1 and 4 talk to the built-in binary storage API directly. Steps 2 and 3 go through a workflow (exposed as an endpoint) because linking and listing are workflow actions.


Prerequisites​

Before you start, make sure you have:

  • An Object Schema that allows attachments, containing the asset you want to attach files to. See Object Attachments for how to enable this.
  • A storage provider linked to that Object Schema, and its provider ID. Copy it from the Manage Storage Provider page.
    • If you do not configure an external storage provider, that's fine — attachments are stored in the gateway database instead. You can still follow this tutorial.
  • Permission to create workflows, endpoints, and a Custom UI.
  • The OBJECT_ID of the asset you want to manage (a Guid).(you can also get it from workflow dynamically while you link attachment)
Authentication — the key advantage of a Custom UI

Because the Custom UI runs inside an authenticated gateway session, calls made from a Custom UI do not need an authorization token — the gateway validates the request automatically.

If you ever call these same endpoints from an external system (a separate app, a script, a backend service), you must create an access token using the External Login API and send it as an Authorization: Bearer <token> header.


Step 1: Get Your Storage Provider ID​

Open Manage Storage Provider, find the provider linked to your asset's Object Schema, and copy its provider ID. You will paste it into the UI code as STORAGE_PROVIDER_ID.


This workflow receives an uploaded file ID and an Object ID, then attaches the file to the Object. Expose it as an endpoint so the Custom UI can call it.

Workflow steps:

  1. Read the request body to get metadataId and objectId (from the endpoint trigger).

  2. LinkAttachment (V1) — pass the values:

    • METADATA_ID = metadataId (the uploaded file ID)
    • OBJECT_ID = objectId (the asset)
    • OVERWRITE = true or false as you prefer
    • RESULT_VAR = e.g. attachmentId
  3. SetHttpResponse — return a small JSON confirmation, for example:

    { "status": "linked", "attachmentId": "<attachmentId>" }

For full parameter details, see the LinkAttachment (V1) action and Set Up an Event Rules / Endpoint.

Where does OBJECT_ID come from?

In this tutorial the Custom UI sends objectId in the request, which is the most common pattern. But the workflow does not have to receive it from the API. OBJECT_ID can also be resolved inside the workflow — for example, by looking the object up with a GetObjectsOfAttributes / LookupAttributes action based on some other value in the request (an asset code, a barcode, a serial number, etc.). Use whichever approach fits your process.

We will assume this endpoint is reachable at:

POST /RCOMENDPOINTAPI/cui/link-attachment

Step 3: Create the "List/Get Attachments" Workflow​

This workflow returns the documents already attached to an asset, so the UI can show them.

Workflow steps:

  1. Read the request body to get objectId.
  2. GetObjectAttachments (V1) — pass OBJECT_ID = objectId, RESULT_VAR = e.g. attachments. This returns a List<Dictionary<string, string>> of metadata (filename, type, and identifiers) — no file bytes.
  3. SetHttpResponse — set CONTENT_TYPE to text/json and return the attachments list as the body.
note

As with the link workflow, OBJECT_ID does not have to come from the API. It can also be resolved inside the workflow using GetObject-style actions if the asset is identified some other way.

See the GetObjectAttachments (V1) action for the exact metadata fields returned.

We will assume this endpoint is reachable at:

POST /RCOMENDPOINTAPI/cui/list-attachments

Step 4: Create the Custom UI​

  1. Navigate to Custom UI → Custom UI Management and click Create New.
  2. Give it a name such as Asset Document Portal, assign the appropriate role, and enable gateway authentication (leave Allow Public Access off) so uploads are tied to a logged-in user.
  3. Save to open the code editor.

For a full walkthrough of creating a Custom UI, see Create Custom UI.


Step 5: Add the HTML​

The HTML gives us a file input, an upload button, a status line, and a container for the document list.

<div class="portal-card">
<h2>Asset Document Portal</h2>
<p class="subtitle">Upload and download inspection reports for this asset.</p>

<div class="upload-row">
<input type="file" id="fileInput" />
<button id="uploadBtn">Upload</button>
</div>

<div class="status" id="status"></div>

<h3>Attached Documents</h3>
<ul class="doc-list" id="docList">
<li class="empty">Loading…</li>
</ul>
</div>

Step 6: Add the CSS​

.portal-card {
max-width: 520px;
margin: 40px auto;
padding: 24px;
border-radius: 12px;
background: #ffffff;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
font-family: Arial, sans-serif;
}

.subtitle { color: #666; margin-top: -6px; }

.upload-row {
display: flex;
gap: 8px;
align-items: center;
margin: 16px 0;
}

#uploadBtn {
padding: 8px 16px;
border: none;
border-radius: 8px;
background: #009688;
color: #fff;
cursor: pointer;
}

#uploadBtn:disabled { background: #9e9e9e; cursor: not-allowed; }

.status { min-height: 20px; font-size: 14px; margin-bottom: 8px; }
.status.ok { color: #0a7a3f; }
.status.err { color: #b00020; }

.doc-list { list-style: none; padding: 0; }

.doc-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
border: 1px solid #eee;
border-radius: 8px;
margin-bottom: 8px;
}

.doc-list a {
color: #009688;
text-decoration: none;
font-weight: 600;
}

.doc-list .empty { justify-content: center; color: #999; border: none; }

Step 7: Add the JavaScript​

This is where everything comes together. Set the three constants at the top to your environment, then wire up upload, list, and download.

// ── Configuration ────────────────────────────────────────────────
const GATEWAY = "https://your-gateway.example.com"; // your gateway base URL
const STORAGE_PROVIDER_ID = "<Storage Provider ID>"; // from Manage Storage Provider
const OBJECT_ID = "<Target OBJECT_ID>"; // the asset this UI manages

// ── 1. Upload the raw file to the storage provider ───────────────
async function uploadFile(file) {
const form = new FormData();
form.append("file", file);
form.append("storageProviderId", STORAGE_PROVIDER_ID);

// No Authorization header needed — the Custom UI session is validated automatically.
const res = await fetch(`${GATEWAY}/RCOMAPI/api/binary-storage/upload`, {
method: "POST",
body: form
});

if (!res.ok) throw new Error(`Upload failed (${res.status})`);
const data = await res.json();
return data.attachmentId; // this is the METADATA_ID
}

// ── 2. Link the uploaded file to the asset (via our workflow) ────
async function linkToAsset(metadataId) {
const res = await fetch(`${GATEWAY}/RCOMENDPOINTAPI/cui/link-attachment`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ objectId: OBJECT_ID, metadataId })
});
if (!res.ok) throw new Error(`Link failed (${res.status})`);
return res.json();
}

// ── 3. Load the asset's document list (via our workflow) ─────────
async function loadDocuments() {
const res = await fetch(`${GATEWAY}/RCOMENDPOINTAPI/cui/list-attachments`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ objectId: OBJECT_ID })
});
if (!res.ok) throw new Error(`List failed (${res.status})`);

const docs = await res.json(); // List<Dictionary<string, string>>
renderDocuments(docs);
}

// ── 4. Build a direct download URL for a file ────────────────────
function downloadUrl(attachmentId) {
return `${GATEWAY}/RCOMAPI/api/binary-storage/download/${attachmentId}`;
}

// ── Render the document list ─────────────────────────────────────
function renderDocuments(docs) {
const list = document.getElementById("docList");
list.innerHTML = "";

if (!docs || docs.length === 0) {
list.innerHTML = `<li class="empty">No documents attached yet.</li>`;
return;
}

docs.forEach(doc => {
// Field names come from GetObjectAttachments metadata.
const name = doc.fileName || "document";
const id = doc.attachmentId;

const li = document.createElement("li");
li.innerHTML = `
<span>${name}</span>
<a href="${downloadUrl(id)}" download="${name}">Download</a>
`;
list.appendChild(li);
});
}

// ── Wire up the upload button ────────────────────────────────────
function setStatus(msg, kind) {
const el = document.getElementById("status");
el.textContent = msg;
el.className = "status " + (kind || "");
}

document.getElementById("uploadBtn").addEventListener("click", async () => {
const input = document.getElementById("fileInput");
const file = input.files[0];
const btn = document.getElementById("uploadBtn");

if (!file) {
setStatus("Please choose a file first.", "err");
return;
}

btn.disabled = true;
setStatus("Uploading…");

try {
const metadataId = await uploadFile(file); // step 1
await linkToAsset(metadataId); // step 2
setStatus(`"${file.name}" uploaded and attached.`, "ok");
input.value = "";
await loadDocuments(); // refresh the list
} catch (err) {
console.error(err);
setStatus(err.message, "err");
} finally {
btn.disabled = false;
}
});

// Load the existing documents when the UI opens.
loadDocuments();
About the metadata field names

GetObjectAttachments (V1) returns each attachment as a Dictionary<string, string>. The identifier used for downloading is the attachmentId key, and the file name is under fileName. Log one entry with console.log(docs[0]) if you want to inspect the full set of available fields.


Step 8: Test the Portal​

  1. Open the Custom UI using its shareable link (while logged in).
  2. Choose a PDF and click Upload. You should see "… uploaded and attached."
  3. The file appears in Attached Documents.
  4. Click Download — the browser fetches the file stream and saves it with its original name.

If an upload fails, check that:

  • STORAGE_PROVIDER_ID matches the provider linked to the asset's Object Schema.
  • The Object Schema allows attachments.
  • OBJECT_ID points to a real asset.

How the Pieces Fit Together​

Action in the UIWhat actually happens
Click UploadFile → binary-storage/upload → returns attachmentId (the METADATA_ID).
(automatically next)attachmentId + OBJECT_ID → workflow → LinkAttachment attaches the file.
List refreshOBJECT_ID → workflow → GetObjectAttachments → metadata rendered as a list.
Click DownloadattachmentId → binary-storage/download/{attachmentId} → file stream to the browser.

The upload and download endpoints are built in. The link and list steps run through your workflows, giving you a place to add validation, logging, or permission checks.


Best Practices​

  • Never hard-code tokens in a Custom UI. You don't need one here — the session is validated automatically. Only external systems need a token via the External Login API.
  • Always link after upload. An uploaded file that is never linked is orphaned in storage but not visible on any Object.
  • Match the storage provider to the Object Schema. Use the provider ID linked to the designated Object Schema.
  • Validate on the server side. Enforce file-type and size rules inside your link workflow, not just in the browser.
  • Preserve filenames. Rely on the download response's Content-Type and Content-Disposition headers so files open correctly.