Huly Integration Guide for Recruitment

Self-hosted ATS setup, Account API, wire-level discoveries, seed paths, and recruitment configuration

AgileX / 2nth.ai Huly v0.7.382 @hcengineering/api-client ^0.7.413 May 2026

Contents

  1. Self-Hosted Stack (14 services)
  2. Account API (undocumented)
  3. Wire-Level Discoveries
  4. Recruit Data Model
  5. Two Seed Paths
  6. Recruitment Configuration
  7. MCP Server Integration
  8. Path B — Google Workspace
  9. Gotchas & Lessons Learned

1. Self-Hosted Stack

Huly self-hosted runs as a 14-container Docker Compose stack on Ubuntu 24.04. We used the --quick flag for local testing on macOS (OrbStack).

Services

ServiceImagePortPurpose
nginxnginx:1.21.38087→80Reverse proxy — routes /_accounts, /api/v1, /_collaborator
fronthardcoreeng/front8080Web UI (SPA)
accounthardcoreeng/account3000Authentication, workspace management, invite flow
transactorhardcoreeng/transactor3333Core data engine (WebSocket). All reads/writes go here.
workspacehardcoreeng/workspaceWorkspace creation, migration, schema upgrades
collaboratorhardcoreeng/collaborator3078Real-time collaborative editing (OT)
fulltexthardcoreeng/fulltext4700Indexes docs → Elasticsearch for search
rekonihardcoreeng/rekoni-service4004AI content intelligence — extracts text from PDFs/DOCX
statshardcoreeng/stats4900Telemetry and health metrics
kvshardcoreeng/hulykvs8094Key-value store for calendar/integrations
cockroachcockroachdb/cockroach v24.226257Primary database (replaced MongoDB in v0.7)
elasticelasticsearch:7.14.29200Full-text search index
miniominio/minio9000S3-compatible file storage (avatars, attachments)
redpandaredpanda v24.3.69092Kafka-compatible message broker

Quick Start

git clone https://github.com/hcengineering/huly-selfhost.git
cd huly-selfhost
./setup.sh --quick
# → Generates config, secrets, starts all 14 containers
# → Huly available at http://localhost:8087 in ~60 seconds

Minimum Resources

ResourceMinimumRecommended
RAM4 GB8 GB (Elasticsearch alone reserves 1 GB heap)
CPU2 vCPU4 vCPU
Disk40 GB SSD80 GB NVMe

2. Account API (Reverse-Engineered)

Huly's Account service exposes a JSON-RPC over HTTP API at /_accounts. This is not publicly documented. We discovered these methods by inspecting the bundled JavaScript in the account container.

Endpoint

POST http://localhost:8087/_accounts
Content-Type: application/json
Authorization: Bearer {token}  (for authenticated methods)

Discovered Methods

signUp — Create a new account

{
  "method": "signUp",
  "params": {
    "email": "admin@agilex.co.za",
    "password": "AgileX2024!",
    "firstName": "Craig",
    "lastName": "Sobey"
  }
}
→ { "result": { "account": "uuid", "token": "jwt", "name": "Craig Sobey" } }

createWorkspace — Create a workspace

{
  "method": "createWorkspace",
  "params": {
    "workspaceName": "AgileX Recruitment",
    "workspaceUrl": "agilex-recruitment"
  }
}
→ { "result": { "token": "workspace-jwt", "workspace": "uuid", "role": "OWNER" } }

Gotcha: Requires workspaceName AND workspaceUrl as separate fields. Using just name returns BadRequest.

createInviteLink — Generate invite for a user

{
  "method": "createInviteLink",
  "params": {
    "email": "thandi@agilex.co.za",
    "role": "OWNER",      // OWNER | MAINTAINER | USER | GUEST
    "expHours": 720
  }
}
→ { "result": "http://localhost:8087/login/join?inviteId=123456" }

Gotcha: The autoJoin parameter exists but requires a "schedule" service permission in the token. Use the two-step flow instead (createInviteLink + join).

join — Accept an invite and join workspace

{
  "method": "join",
  "params": {
    "email": "thandi@agilex.co.za",
    "password": "AgileX2024!",
    "inviteId": "123456"
  }
}
→ { "result": { "token": "workspace-jwt", "workspace": "uuid" } }

getWorkspaceMembers — List workspace members

{
  "method": "getWorkspaceMembers",
  "params": {}
}
→ { "result": [{ "person": "uuid", "role": "OWNER" }, ...] }

Other Methods Found (not all tested)

MethodPurposeVerified
loginAuthenticate existing accountYes
selectWorkspaceSwitch workspace contextYes
getWorkspaceInfoGet workspace detailsYes
assignWorkspaceAssign user to workspaceForbidden (needs admin)
deleteAccountDelete user accountNot tested
deleteWorkspaceDelete workspaceNot tested
getWorkspaceRoleGet user's role in workspaceNot tested

Roles

RoleValuePermissions
OwnerOWNERFull admin — invite, manage, delete
MaintainerMAINTAINERManage content, invite users
UserUSERStandard access — read/write content
GuestGUESTRead-only view access
Read-only GuestREADONLYGUESTStrict read-only (no comments)

3. Wire-Level Discoveries

We wrote probe scripts (scripts/discover.ts and scripts/probe.ts) that connect to a live Huly workspace via @hcengineering/api-client and query the data model. These findings are not documented anywhere in Huly's public docs.

Key Findings (verified 2026-04-26)

Q1: Where do Vacancies live?

Answer: space = core:space:Space (global). type = recruit:template:DefaultVacancy. A Vacancy IS a Space (extends task:class:Project).

Q2: How do pipeline statuses work?

Answer: Each Vacancy carries a statuses[] array of refs. The 7 default statuses are shared, stable refs across all Vacancies created from the DefaultVacancy template. They are NOT auto-seeded by createDoc — the MCP server must replicate them manually.

Q3: How are contact channels stored?

Answer: Person has no inline channels field. Each channel (email, phone, LinkedIn) is a separate contact:class:Channel doc added via addCollection on the Person. Drive URLs are stored as Homepage channels — same as a recruiter pasting a link in the UI.

Q4: How does the Candidate mixin work?

Answer: A Person becomes a Talent by attaching recruit:mixin:Candidate via createMixin(personId, Person, Candidate, {}). Persons can carry multiple mixins (Employee, HR Staff, Candidate simultaneously).

Q5: Person name convention

Answer: Huly stores Person.name as "LastName,FirstName" (comma-separated, no space). The UI renders it as "FirstName LastName".

Q6: Avatar fields

Answer: avatarType and avatarProps are server-defaulted (colour avatar). Not required from the client on createDoc.

4. Recruit Data Model

Person (contact:class:Person)
  name: "LastName,FirstName"
  city: string
  ├── recruit:mixin:Candidate  { title?, source? }
  ├── Channel[] (Email, Phone, LinkedIn, Homepage)
  └── Application[] (one per Vacancy applied to)

Vacancy (recruit:class:Vacancy extends task:class:Project)
  name: string (job title)
  description: string (markdown)
  type: recruit:template:DefaultVacancy
  statuses: Status[] (7 default pipeline stages)
  members: [], owners: [], private: false, archived: false
  ├── Applicant[] (one per Talent in this pipeline)

Applicant (recruit:class:Applicant)
  space: Vacancy (Vacancy IS a Space)
  attachedTo: Person (Talent)
  collection: 'applications'
  status: recruit:taskTypeStatus:* (pipeline stage ref)
  assignee?: Employee ref
  identifier: string (auto-generated)

Pipeline Stages (Default)

#StageWire RefCategory
1Backlogrecruit:taskTypeStatus:BacklogUnStarted
2HR Interviewrecruit:taskTypeStatus:HRInterviewActive
3Technical Interviewrecruit:taskTypeStatus:TechnicalInterviewActive
4Test Taskrecruit:taskTypeStatus:TestTaskActive
5Offerrecruit:taskTypeStatus:OfferActive
6Wonrecruit:taskTypeStatus:WonWon
7Lostrecruit:taskTypeStatus:LostLost

Channel Providers

ProviderWire RefUsed For
Emailcontact:channelProvider:EmailCandidate email
Phonecontact:channelProvider:PhonePhone number
LinkedIncontact:channelProvider:LinkedInLinkedIn profile URL
Homepagecontact:channelProvider:HomepageGoogle Drive URLs (CV, folder, notes)
GitHubcontact:channelProvider:GitHubGitHub profile
Telegramcontact:channelProvider:TelegramTelegram handle
WhatsAppcontact:channelProvider:WhatsappWhatsApp number

5. Two Seed Paths

We implemented two different approaches to populating a Huly recruitment workspace:

Path A — Direct SQL to CockroachDB

Insert structured data directly into CockroachDB tables (contact, hr, recruit, space) bypassing the transactor. Fast but fragile.

Path B — MCP via @hcengineering/api-client Recommended

Uses the official Huly client library over WebSocket. Creates docs through the transactor — triggers all server-side effects (indexing, notifications, counters).

6. Recruitment Configuration

Workspace: AgileX Recruitment

SettingValue
Workspace URLagilexrecruitment
Huly versionv0.7.382
Accesshttp://localhost:8087
Adminadmin@agilex.co.za / AgileX2024!

Staff (11 members)

NameRoleDepartmentAccess
Craig SobeyPlatform AdminOWNER
Thandi MokoenaManaging DirectorLeadershipOWNER
Pieter van der MerweHead of RecruitmentLeadershipMAINTAINER
Naledi KhumaloSenior Recruitment MgrLeadershipMAINTAINER
Siyanda DlaminiBanking SpecialistConsultantsUSER
James ReynoldsTech & AI SpecialistConsultantsUSER
Fatima PatelGraduate Programme LeadConsultantsUSER
Bongani NdlovuRetail SpecialistConsultantsUSER
Sarah O'ConnorPM SpecialistConsultantsUSER
Mpho SitholeResearch & SourcingSupportUSER
Lerato MahlanguAdmin & CoordinationSupportUSER

All staff emails: firstname.lastname@agilex.co.za / Password: AgileX2024!

HR Departments

DepartmentMembers
LeadershipThandi, Pieter, Naledi
ConsultantsSiyanda, James, Fatima, Bongani, Sarah
SupportMpho, Lerato

MCP Demo Data (V3 Seed)

TalentTitleCityStage
Kagiso MaileSenior ML EngineerJohannesburgBacklog
Chloé DuboisSenior ML EngineerCape TownHR Interview
Rudzani NemukulaStaff MLOps EngineerPretoriaTechnical Interview
Farhan IsmailPrincipal AI EngineerCape TownTest Task
Tshepo MabasoSenior ML EngineerJohannesburgOffer

Vacancy: V3 · ML Platform Engineer (LLM/MLOps) — R1.4M–R1.8M CTC, Cape Town hybrid.

7. MCP Server Integration

See the full MCP specification: Huly Recruit MCP Spec

Quick Reference

ToolInput (required)Creates
create_talentfirst_name, last_namePerson + Candidate mixin + channels
create_vacancynameVacancy with 7 pipeline stages
create_applicationtalent_id, vacancy_id, stageApplicant linking talent → vacancy at stage

8. Path B — Google Workspace as Document Store

Huly stores metadata and links only. All durable artifacts live in Google Workspace.

Huly StoresGoogle Workspace StoresLink Mechanism
Talent name, title, cityFull CV (PDF)Homepage channel with Drive URL
Channel refs (email, phone)Candidate folderHomepage channel with folder URL
Vacancy name, descriptionFull vacancy brief (Doc)URL appended to description
Application stage, assigneeInterview scorecards (Sheet)
Pipeline statusOffer letters, contracts

Google Drive Folder Structure

AgileX Recruitment/
├── Candidates/
│   ├── Active/
│   │   └── {FirstName LastName}/
│   │       ├── CV.pdf
│   │       ├── References/
│   │       └── Notes.gdoc
│   ├── Placed/
│   └── Archive/
├── Clients/
│   └── {Client Name}/
│       ├── Contract.pdf
│       └── Vacancy_Briefs/
├── Templates/
└── Reports/

9. Gotchas & Lessons Learned

Account service can't reach localhost from inside Docker

The account service's STATS_URL and FRONT_URL default to http://localhost:8087 — unreachable from inside a container. Fix: set STATS_URL=http://stats:4900 in compose.yml.

@hcengineering/api-client is CJS-only

The published npm package uses esbuild's __reExport pattern. Neither named imports nor namespace imports work through Node's ESM loader. Fix: use createRequire(import.meta.url) to load it.

Vacancy createDoc doesn't seed pipeline statuses

The Huly UI's "Create Vacancy" flow auto-seeds statuses from the DefaultVacancy template. createDoc via the API does NOT trigger this. You must manually include the 7 default statuses in the statuses[] array.

Vacancy requires Space fields or it crashes

Because Vacancy extends Project which extends TypedSpace, it needs members: [], owners: [], private: false, archived: false or the server errors with "space.members is not iterable".

Password hash gotcha

Huly passwords containing # characters are silently truncated by some shell env parsers. Use .env.local files with --env-file-if-exists flag instead of inline env vars.

WebSocket doesn't work in Cloudflare Workers

@hcengineering/api-client uses Node's ws library for WebSocket. This doesn't run in Cloudflare Workers even with nodejs_compat. For edge integration, wrap the MCP server in a thin HTTP service that Workers can call via fetch().

Skills not yet attachable via API

The @hcengineering/tags subsystem handles skills/labels. The create_talent tool accepts skills as input but can't attach them yet — the tags subsystem needs a separate investigation.


AgileX Huly Integration Guide · May 2026 · agilex.co.za · 2nth.ai