skills/custom-apps/fileset-api/SKILL.md
Build Domo custom apps that use FileSets for file storage, upload, download, and AI document search — all via fetch('/api/files/v1/') with no manifest.json entry required. Use this whenever a Domo app needs file upload via FormData/fetch (not domo.post), file download as a blob, file listing via POST /files/search, AI document queries via the /query endpoint, or fileset CRUD. Also use when the user asks about /api/files/v1 vs /domo/files/v1, whether filesets need manifest.json wiring, how to build a file browser in React, or how to make a document Q&A chatbot in Domo.
npx skillsauth add stahura/domo-ai-vibe-rules fileset-apiInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Status: This API is in BETA and subject to change. Build defensively — wrap calls in try/catch and surface meaningful errors to the user.
FileSets are Domo's file storage system. From a custom app you can upload and download files, browse directory trees, search by name, and run AI-powered semantic queries against file content.
Unlike datasets, filesets don't need a manifest.json entry. Just define the fileset ID as a constant in your service file:
// src/services/api.js
const FILESET_ID = 'b6ebf7e9-64ae-4e6d-b8ca-b356fe62923f'; // replace with your fileset ID
Get the fileset ID from the Domo UI or via the fileset-cli skill (filesets search --name "...").
/api/ vs /domo/Within a Domo custom app you have two valid approaches:
| Pattern | When to use |
|---------|-------------|
| fetch('/api/files/v1/...') | Simplest. Use for all JSON and binary calls. No import needed. |
| domo.get('/domo/files/v1/...') | Use when you want the domo.js proxy (e.g. already using domo.* elsewhere). JSON only — no binary. |
The examples below use fetch('/api/...') because it handles both JSON and binary (uploads/downloads) consistently. For binary operations you must use fetch regardless.
const response = await fetch(`/api/files/v1/filesets/${FILESET_ID}/files/search`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fieldSort: [{ field: 'created', order: 'DESC' }],
filters: [],
dateFilters: []
})
});
const data = await response.json();
const files = data.files; // array of file objects
With directory and name filter:
const response = await fetch(
`/api/files/v1/filesets/${FILESET_ID}/files/search` +
`?directoryPath=/reports&immediateChildren=true&limit=50`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fieldSort: [{ field: 'name', order: 'ASC' }],
filters: [{ field: 'name', value: ['.pdf'], operator: 'LIKE' }],
dateFilters: []
})
}
);
Paginate using the next token from data.pageContext.next:
// data.pageContext shape: { next, offset, limit, total }
if (data.pageContext.next) {
const nextPage = await fetch(
`/api/files/v1/filesets/${FILESET_ID}/files/search?next=${data.pageContext.next}`,
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }
);
}
// By file ID
const response = await fetch(`/api/files/v1/filesets/${FILESET_ID}/files/${fileId}`);
const file = await response.json();
// { id, path, name, fileType, contentType, size, hash, created, createdBy }
// By path (when you know the directory structure)
const response = await fetch(
`/api/files/v1/filesets/${FILESET_ID}/path?path=${encodeURIComponent('/reports/march.pdf')}`
);
Use fetch and create a temporary download link. The download endpoint returns binary content:
async function downloadFile(fileId, filename) {
const response = await fetch(
`/api/files/v1/filesets/${FILESET_ID}/files/${fileId}/download`
);
if (!response.ok) throw new Error(`Download failed: ${response.status}`);
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
For text-based files you can also use .text() instead of .blob() to read content directly:
const response = await fetch(`/api/files/v1/filesets/${FILESET_ID}/files/${fileId}/download`);
const text = await response.text(); // use for .txt, .csv, .md, etc.
Use FormData with two parts — the binary file and the directory metadata. Don't set Content-Type; the browser sets it with the correct multipart boundary automatically:
async function uploadFile(file, directoryPath = '/') {
const formData = new FormData();
formData.append('file', file); // File or Blob
formData.append('createFileRequest', JSON.stringify({ directoryPath })); // metadata
const response = await fetch(`/api/files/v1/filesets/${FILESET_ID}/files`, {
method: 'POST',
body: formData
// ⚠️ Do NOT set Content-Type header — browser handles it
});
if (!response.ok) throw new Error(`Upload failed: ${response.status}`);
return response.json(); // returns the new file object
}
From a file input element:
document.getElementById('file-input').addEventListener('change', async (e) => {
const file = e.target.files[0];
const record = await uploadFile(file, '/uploads/2024');
console.log('Uploaded:', record.id, record.path);
});
From a generated Blob (e.g. CSV export):
const csv = 'name,value\nAlice,100\nBob,200';
const blob = new Blob([csv], { type: 'text/csv' });
const file = new File([blob], 'export.csv', { type: 'text/csv' });
await uploadFile(file, '/exports');
// By file ID
await fetch(`/api/files/v1/filesets/${FILESET_ID}/files/${fileId}`, { method: 'DELETE' });
// By path
await fetch(
`/api/files/v1/filesets/${FILESET_ID}/path?path=${encodeURIComponent('/reports/march.pdf')}`,
{ method: 'DELETE' }
);
You can create and manage fileset containers from within the app, though typically the fileset already exists and the app just uses the hardcoded ID.
const response = await fetch('/api/files/v1/filesets/search?limit=50', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fieldSort: [{ field: 'updated', order: 'DESC' }],
filters: [{ field: 'name', value: ['reports'], operator: 'LIKE' }],
dateFilters: []
})
});
const response = await fetch('/api/files/v1/filesets', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Monthly Reports', description: 'Automated PDF outputs' })
});
const fileset = await response.json();
const filesetId = fileset.id;
Requires aiEnabled: true on the fileset. Runs a natural-language question against file content and returns ranked matches with relevance scores:
const response = await fetch(`/api/files/v1/filesets/${FILESET_ID}/query`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: 'What were the key revenue drivers last quarter?',
directoryPath: '/reports/2024',
topK: 5
})
});
const data = await response.json();
// data.matches: [{ id, node: { file object }, score: 0.89 }, ...]
{
"id": "xyz789-file-id",
"path": "/reports/2024/march.pdf",
"name": "march.pdf",
"fileType": "PDF",
"contentType": "application/pdf",
"size": 204800,
"hash": "sha256:abc...",
"hashAlgorithm": "SHA256",
"created": "2024-03-01T08:00:00Z",
"createdBy": 12345
}
A clean service module pattern for a React app:
// src/services/filesetApi.js
const FILESET_ID = 'your-fileset-id-here';
const BASE = `/api/files/v1/filesets/${FILESET_ID}`;
export async function listFiles(directoryPath = null, nameFilter = null) {
const params = new URLSearchParams({ limit: '100' });
if (directoryPath) params.set('directoryPath', directoryPath);
const filters = nameFilter
? [{ field: 'name', value: [nameFilter], operator: 'LIKE' }]
: [];
const response = await fetch(`${BASE}/files/search?${params}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fieldSort: [{ field: 'name', order: 'ASC' }], filters, dateFilters: [] })
});
if (!response.ok) throw new Error(`List failed: ${response.status}`);
const data = await response.json();
return data.files ?? [];
}
export async function downloadFileContent(fileId) {
const response = await fetch(`${BASE}/files/${fileId}/download`);
if (!response.ok) throw new Error(`Download failed: ${response.status}`);
return response.text(); // swap for .blob() for binary files
}
export async function uploadFile(file, directoryPath = '/') {
const form = new FormData();
form.append('file', file);
form.append('createFileRequest', JSON.stringify({ directoryPath }));
const response = await fetch(`${BASE}/files`, { method: 'POST', body: form });
if (!response.ok) throw new Error(`Upload failed: ${response.status}`);
return response.json();
}
export async function queryFiles(question, directoryPath = null, topK = 5) {
const body = { query: question, topK };
if (directoryPath) body.directoryPath = directoryPath;
const response = await fetch(`${BASE}/query`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!response.ok) throw new Error(`Query failed: ${response.status}`);
const data = await response.json();
return data.matches ?? [];
}
To discover fileset IDs, browse files, download to disk, or run commands outside an app, use the fileset-cli skill.
tools
Step-by-step orchestrator for building Domo App Studio apps with native KPI cards via community-domo-cli. Sequences app creation, pages, theme, hero metrics, native charts, filter cards, layout assembly, and navigation. CLI-first — no raw API calls.
tools
Create, update, and execute Magic ETL dataflows programmatically via API and CLI. Covers DAG-based JSON dataflow definitions, input/transform/output node wiring, join operations, and execution lifecycle.
tools
Magic ETL dataflows via community-domo-cli — list, get-definition, create, update, run, execution status; JSON DAG actions, transforms, joins. Use when automating dataflows with the community Domo CLI end-to-end. For REST/Java-CLI–first flows or mixed API patterns, use magic-etl instead.
development
Clean, professional dashboard theme for Domo custom apps. CSS custom properties, layout patterns, typography, and design polish that feel native to the Domo platform. Includes OKLCH color palette, layered shadows, concentric border radius, tabular numbers, and micro-interaction patterns.