Hudu Articles — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Hudu Articles (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Articles in Hudu serve as the knowledge base, providing a place for runbooks, procedures, network diagrams, SOPs, and general documentation. Articles support rich HTML content, can be organized into folders, and can be scoped to specific companies or kept as global (shared across all companies). MSP technicians rely on articles to quickly find procedures and reference documentation during troubleshooting.
use itglue-documents. Both platforms say "article", "document", and "runbook" interchangeably, so the vendor name is the only signal.
(make, model, IP, expiry) it belongs on an asset layout, not in article HTML; use hudu-assets.
endpoint and their own audit trail; use hudu-passwords. Do not let an agent paste a credential into article body HTML to "keep it together".
PSA, not in the knowledge base; use autotask-ticket-notes-attachments or connectwise-psa-tickets.
Articles can be scoped in two ways:
| Scope | Description | Use Case |
|---|---|---|
| Company-specific | Tied to a single company | Network diagram for Acme Corp |
| Global | Available across all companies | Standard new user setup procedure |
Scope is controlled entirely by company_id — omit it (or set it to null) to create a global article.
Folders organize articles within a company or globally, and can be nested via parent_folder_id:
Company: Acme Corporation
+-- Articles
+-- Procedures
| +-- Backup Procedure
| +-- Disaster Recovery Plan
+-- Network
| +-- Network Overview
| +-- IP Addressing Scheme
+-- Onboarding
+-- New User Setup
+-- Hardware DeploymentArticle content is stored as HTML. Hudu's editor supports:
Articles can be saved as drafts before publishing:
| State | Description |
|---|---|
| Draft | Work in progress, not visible to all users |
| Published | Visible to users with appropriate permissions |
Key fields: id, company_id, name (required), content (HTML), folder_id, draft, slug, created_at, updated_at, url.
See references/fields.md for the complete field reference.
| Operation | Request | |
|---|---|---|
| List / filter | GET /api/v1/articles?company_id=123&name=backup&page=1 | |
| Get one | GET /api/v1/articles/456 | |
| Create | POST /api/v1/articles with { "article": { ... } } | |
| Update | PUT /api/v1/articles/456 | |
| Delete | DELETE /api/v1/articles/456 | |
| Archive | PUT /api/v1/articles/456/archive | |
| Folders | `GET | POST /api/v1/folders (filter with ?company_id=`) |
All requests use the x-api-key header. Request and response bodies are wrapped in a singular resource key (article, folder).
See references/api.md for the complete endpoint catalog with request/response examples.
async function createRunbook(companyId, runbookData) {
// Ensure folder exists
const folder = await ensureFolder(companyId, runbookData.folderPath);
// Build content
let content = `<h1>${runbookData.title}</h1>`;
content += `<h2>Overview</h2><p>${runbookData.overview}</p>`;
if (runbookData.prerequisites?.length) {
content += `<h2>Prerequisites</h2><ul>`;
content += runbookData.prerequisites.map(p => `<li>${p}</li>`).join('');
content += `</ul>`;
}
if (runbookData.steps?.length) {
content += `<h2>Procedure</h2><ol>`;
content += runbookData.steps.map(s => `<li>${s}</li>`).join('');
content += `</ol>`;
}
// Create the article
return await createArticle({
name: runbookData.title,
company_id: companyId,
folder_id: folder?.id,
content: content
});
}The API filters on name only — full-text search across content must be done client-side after fetching.
async function searchArticles(companyId, query) {
const articles = await fetchArticles({ company_id: companyId });
const queryLower = query.toLowerCase();
return articles.filter(article =>
article.name.toLowerCase().includes(queryLower) ||
article.content?.toLowerCase().includes(queryLower)
);
}async function documentationHealthCheck(companyId) {
const articles = await fetchArticles({ company_id: companyId });
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const yearAgo = new Date();
yearAgo.setFullYear(yearAgo.getFullYear() - 1);
return {
totalArticles: articles.length,
drafts: articles.filter(a => a.draft).length,
recentlyUpdated: articles.filter(a =>
new Date(a.updated_at) > thirtyDaysAgo
).length,
stale: articles.filter(a =>
new Date(a.updated_at) < yearAgo
).map(a => ({
name: a.name,
lastUpdated: a.updated_at
})),
empty: articles.filter(a =>
!a.content || a.content.trim().length < 50
).map(a => a.name)
};
}Folder IDs are company-scoped, so do not carry folder_id across companies — resolve or create a folder in the target company first.
async function cloneArticle(articleId, targetCompanyId, newName) {
const template = await getArticle(articleId);
return await createArticle({
name: newName || template.name,
company_id: targetCompanyId,
content: template.content
});
}Standard HTML skeletons for Network Overview and Disaster Recovery Plan articles are in references/templates.md.
folder_id belonging to another company yields a 422; on failure, retry without folder_id to create at root level.content are not escaped by the API; sanitize any untrusted input before writing.PUT /articles/:id/archive), not an archived field on update.See references/errors.md for the complete error and validation table plus a recovery pattern.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.