Schedule autonomous AI agents in Nextcloud
Hermiq brings autonomous AI agents to Nextcloud. Define an agent, give it tools, and run it on a schedule — all inside your own Nextcloud. Agents, their memory and skills are stored as OpenRegister objects, so every run is governed, auditable and multi-tenant.
Manifest-first — pages, navigation, and dependencies are declared in
src/manifest.json. The shell (CnAppRoot) reads the manifest at boot and renders index / detail / dashboard / settings pages without per-page Vue files. Reach for a custom Vue component only when the page istype: "custom". Seeopenspec/architecture/and hydra ADR-024 for the architectural rationale.
Pre-wired for OpenRegister —
manifest.dependencieslistsopenregister, so CnAppRoot's dependency-check phase ensures the OR app is installed and enabled before the UI mounts. If your app does not need OpenRegister, remove the entry fromsrc/manifest.json,appinfo/info.xml, andopenspec/app-config.json.
Canonical root configs —
phpcs.xml,phpmd.xml,psalm.xml,phpstan.neon, andphpstan-bootstrap.phpin this repo are the fleet canonical. All Conduction PHP apps are expected to mirror these files byte-for-byte; per-app deviations belong in baselines (phpstan-baseline.neon,psalm-baseline.xml) not in the canonical files. Submit changes here and they propagate to the fleet via the template-sync flow — do not diverge per-app.
Screenshots are captured automatically from the tutorial flows, not pasted in by hand. The journeydoc scaffold (hydra ADR-030) ships two tutorial stories under docs/tutorials/ — a user "first launch" walkthrough (the Dashboard) and an admin "manage settings" walkthrough (Admin Settings) — and a Playwright docs-capture project that turns each documented step into a PNG under docs/static/screenshots/tutorials/.
- Add a tutorial story:
/journeydoc-add-story - Add stable
data-testidhooks to a Vue component:/journeydoc-instrument - (Re)capture screenshots against a running Nextcloud:
NEXTCLOUD_URL=http://localhost:8080 npx playwright test --project docs-capture
See the Documentation section below for how the docs site itself is built and deployed.
Features are defined in openspec/specs/. See the roadmap for planned work.
- Dashboard — Personal overview page with key information at a glance
- Admin Settings — Configurable settings panel for administrators
- OpenRegister Integration — Pre-wired data layer using OpenRegister objects
- Quality Pipeline — PHPCS, PHPMD, Psalm, PHPStan, ESLint, Stylelint
graph TD
A[Vue 2 Frontend] -->|REST API| B[OpenRegister API]
B --> C[(PostgreSQL JSON store)]
A --> D[Nextcloud Activity]
A --> E[Nextcloud Search]
Update this diagram during /app-explore sessions as the architecture evolves.
| Object | Description |
|---|---|
| (define your data objects here) | — |
Data model is defined using OpenRegister schemas. See openspec/specs/ for feature-level design decisions and openspec/architecture/ for architectural decisions.
hermiq/
├── appinfo/ # Nextcloud app manifest, routes, navigation
├── lib/ # PHP backend
│ ├── AppInfo/Application.php
│ ├── Controller/ # DashboardController, SettingsController
│ ├── Dashboard/ # Nextcloud Dashboard widget classes (ExampleWidget)
│ ├── Mcp/ExampleToolProvider.php # AI Chat Companion tools (hydra ADR-034/035)
│ ├── Service/SettingsService.php
│ ├── Listener/DeepLinkRegistrationListener.php
│ ├── Repair/InitializeSettings.php
│ └── Settings/ # AdminSettings, hermiq_register.json
├── templates/ # PHP templates (SPA shells)
├── src/ # Vue 2 frontend
│ ├── manifest.json # Pages + menu + dependencies (v2 — the source of truth)
│ ├── main.js # App entry — bootstraps CnAppRoot from manifest
│ ├── App.vue # Mounts CnAppRoot + #sidebar slot
│ ├── registry.js # v2 five-kind component registry (widget/modal/page/form-field/cell-renderer)
│ ├── customComponents.js # v1 registry (kept for backward-compat; remove once v2 migration complete)
│ ├── widgets/ # kind: "widget" components (ExampleWidget.vue)
│ ├── modals/ # kind: "modal" components (ExampleModal.vue)
│ ├── formFields/ # kind: "form-field" components (EmailField.vue)
│ ├── cellRenderers/ # kind: "cell-renderer" components (StatusBadge.vue)
│ ├── exampleWidget.js # Sample Nextcloud Dashboard widget entry-point
│ ├── settings.js # Nextcloud admin settings webpack entry-point
│ ├── store/ # Pinia stores (used by AdminSettings)
│ ├── views/CustomExample.vue # Example custom component (registry demo)
│ └── views/widgets/ # Dashboard widget Vue components (ExampleWidget.vue)
├── openspec/ # Specifications, decisions, and roadmap
│ ├── app-config.json # Canonical app config (id, goal, dependencies, CI)
│ ├── config.yaml # OpenSpec CLI configuration
│ ├── specs/ # Feature specs (input for OpenSpec changes)
│ ├── architecture/ # App-specific Architectural Decision Records
│ ├── ROADMAP.md # Product roadmap
│ └── changes/ # OpenSpec change directories (created on first change)
├── docs/ # Docusaurus documentation site (@conduction/docusaurus-preset)
│ ├── docusaurus.config.js # Site config — title, url, navbar, brand theme
│ ├── intro.md # Docs landing page
│ ├── src/pages/index.js # Marketing landing page (brand <DetailHero> / <WidgetShelf>)
│ ├── tutorials/ # journeydoc tutorial stories — user/ + admin/ tracks (ADR-030)
│ └── static/screenshots/ # Captured tutorial screenshots (populated by docs-capture)
├── tests/ # Unit and integration tests
│ ├── e2e/docs-screenshots.spec.ts # journeydoc screenshot capture suite (Playwright docs-capture project)
│ ├── validate-manifest.js # Ajv schema validator for src/manifest.json (nc-vue manifest schema)
│ ├── validate-register.js # Structural validator for lib/Settings/*_register.json (slugs, lifecycle requires→PHP, clobber heuristic)
│ └── validate-json-strict.js # Strict JSON parse — rejects duplicate keys + appendOnly nested in x-openregister
├── playwright.config.ts # Playwright config — `chromium` (regression) + `docs-capture` (screenshots) projects
├── l10n/ # Translations (en, en_US, nl)
├── .github/workflows/ # CI/CD pipelines (incl. documentation.yml — deploys docs/ from `development`)
├── Makefile # Dev helpers (make dev-link)
└── img/ # App icons and screenshots
| Dependency | Version |
|---|---|
| Nextcloud | 28 – 33 |
| PHP | 8.1+ |
| Node.js | 20+ |
| OpenRegister | latest |
- Go to Apps in your Nextcloud instance
- Search for Hermiq
- Click Download and enable
OpenRegister must be installed first. Install OpenRegister →
cd /var/www/html/custom_apps
git clone https://codeberg.org/Conduction/hermiq.git hermiq
cd hermiq
npm install && npm run build
php occ app:enable hermiqdocker compose -f ../openregister/docker-compose.yml up -dnpm install
npm run dev # Watch mode
npm run build # Production build
npm run check:manifest # Validate src/manifest.json against the nc-vue manifest schema
npm run check:register # Validate lib/Settings/*_register.json (schema shape, slugs, lifecycle requires → PHP class exists)
npm run check:json-strict # Strict JSON parse of the config files — fails on duplicate keys (the silent-data-loss class of bug a bad git merge produces)
npm run check:specs # All three of the above — run this before committing any register/manifest changeWhy
check:specs?gitmerges JSON files line-by-line — two branches that both add an object at the same key but at different file positions produce no textual conflict, just a document with a duplicate key, andjson_decode()keeps the last one (so the earlier, fuller definition is silently dropped).check:json-strictfails CI on that.check:registeralso catches a lifecyclerequires:pointing at a PHP class that doesn't exist, andappendOnlynested inx-openregister(which OpenRegister silently ignores). TheSpec ValidationGitHub workflow runscheck:specson every push/PR; add it to your branch-protection ruleset's required checks to make it block merges.
The Settings menu entry uses action: "user-settings" → opens NcAppSettingsDialog via CnAppRoot's cnOpenUserSettings inject; feed your settings sections into App.vue's #user-settings slot.
Pages live in src/manifest.json — NOT in
src/views/. To add a page, edit the manifest:
-
Add a menu entry in
manifest.menu(id, label, icon, route). -
Add a page in
manifest.pages(id matches the menu'sroute). Pick thetype:Type Use when dashboardHome / overview with KPI widgets indexSchema-backed list view (CnDataTable + sidebar) detailSingle-object view at /things/:idsettingsAdmin/user settings with version-info/register-mappinglogsTail-style audit log view chatConversation thread page filesFolder browser customBespoke Vue component (last resort) -
Run
npm run check:manifestto verify the manifest validates.
You only write a Vue file when the page is type: "custom". In that
case, drop the component into src/views/, register it in
src/customComponents.js, and reference
its registry name in the manifest entry's component field. See
src/views/CustomExample.vue for the
canonical example.
Search-and-replace hermiq (the <id> from appinfo/info.xml)
in: appinfo/info.xml, package.json, openspec/app-config.json,
src/main.js (the app-id prop, the loadTranslations arg, and the
generateUrl base path), src/App.vue (the app-id prop and
translateForApp argument), and webpack.config.js (the appId
constant). The manifest itself does not carry the app id.
The scaffold ships a v2 manifest by default (src/manifest.json declares
$schema pointing to the v2 schema URL). V2 collapses the four v1 widget
shapes into a single uniform widgets[] array with grid coordinates on every
page type, and introduces a five-kind component registry.
Design reference: hydra ADR-036 (Universal Widget Manifest v2)
Migration guide (for apps migrating from v1): @conduction/nextcloud-vue
docs migrating-to-v2.md covers the codemod CLI and manual migration steps.
The v2 way to add custom UI is src/registry.js. It has five kinds:
| kind | Use when |
|---|---|
widget |
Custom placeable widget — add to any page's widgets[] via widgetKey |
modal |
Dialog opened by manifest actions with type: "open-modal" |
page |
Bespoke full-page component (type: "custom" in the manifest — use sparingly) |
form-field |
Custom form input auto-bound by JSON Schema format |
cell-renderer |
Custom table-cell rendering auto-bound by schema + property |
Steps to add a custom widget:
- Create
src/widgets/<YourWidget>.vue. - Add an entry to
src/registry.jswithkind: "widget"+defaultSize,minSize,maxSize,allowedSlots,propsSchema. - Add a
widgets[]entry to the target page insrc/manifest.jsonwithwidgetKey: "<your-key>",slot, and grid coordinates. - Run
npm run check:manifest-v2to validate.
The scaffold ships one example per kind as a starting point. Delete or replace the examples when you clone the template.
Backward compat:
src/customComponents.jsis kept for the v1 → v2 transition period. Both thecustomComponentsandregistryprops coexist onCnAppRoot. Once fully migrated,customComponents.jsand its import inmain.jscan be removed.
The template ships with a working ExampleWidget you can copy. Each widget is
three files plus two registration points:
lib/Dashboard/<Foo>Widget.php— implementsOCP\Dashboard\IWidget. Theload()method MUST attach the two shared chunks (-shared-vendor,-shared-nc-vue) before the per-widget bundle. Order matters; seeExampleWidget.phpfor the reference.src/<foo>Widget.js— webpack entry-point that registers the renderer viaOCA.Dashboard.register('<id>', (el, { widget }) => { ... }). The id MUST equalWidget::getId()from PHP.src/views/widgets/<Foo>Widget.vue— the renderer itself. Wrap in<NcDashboardWidget>for free loading + empty states.- Register in
lib/AppInfo/Application.php: add$context->registerDashboardWidget(<Foo>Widget::class);. - Add a webpack entry in
webpack.config.jssonpm run buildproduces<appId>-<foo>Widget.js.
The optimization.splitChunks block in webpack.config.js extracts shared
framework code (Vue, @nextcloud/vue, pinia, icons) into two chunks loaded
once across the page. Without it every widget bundle would inline ~3 MB of
duplicated framework code per entry-point. See ADR-004 (Build / bundling)
in the hydra repo for the full rationale.
The template ships an example MCP tool provider so the in-app AI Chat
Companion (a floating assistant rendered by CnAppRoot from
@conduction/nextcloud-vue) can call your app's capabilities. See:
lib/Mcp/ExampleToolProvider.php— the heavily-commented starting point. It implementsOCA\OpenRegister\Mcp\IMcpToolProviderand exposes two trivial tools:hermiq.ping(echoes a message) andhermiq.describeApp(returns the app id, version, and name).lib/AppInfo/Application.php— registers the provider under the service aliasOCA\OpenRegister\Mcp\IMcpToolProvider::{appId}; OpenRegister'sMcpToolsServicediscovers per-app providers by exactly that alias.tests/Stubs/Mcp/IMcpToolProvider.php— a stand-in for the interface until openregister PR #1466 merges; once the openregister app is installed alongside your app the real interface takes over transparently.tests/Unit/Mcp/ExampleToolProviderTest.php— the contract test.
To wire up your app:
- Rename
ExampleToolProvider→{YourApp}ToolProvider(update the alias inApplication.phpand the test). - Replace the two example tools with real ones — each descriptor needs
id({appId}.{toolName}),name,description, and aninputSchema(JSON Schema object). - In
invokeTool(), run per-object authorisation before any business logic or data access — validate args, then authorise, then delegate, then return.invokeTool()MUST NOT throw — every failure path returns a structured['error' => ['code' => ..., 'message' => ...]]array.
References: hydra
ADR-034 (AI Chat Companion)
and ADR-035; and decidesk's OCA\Decidesk\Mcp\DecideskToolProvider as the
production example (five real tools, deep links, source descriptors).
# PHP
composer check:strict # All quality checks (PHPCS, PHPMD, Psalm, PHPStan, tests)
composer cs:fix # Auto-fix PHPCS issues
composer phpmd # Mess detection
composer phpmetrics # HTML metrics report
# Frontend
npm run lint # ESLint
npm run stylelint # CSS lintingNextcloud requires the app directory name to match the <id> in appinfo/info.xml (hermiq).
When this repo is cloned as hermiq, no symlink is needed.
Note: The
js/build output is not committed. You must build the frontend before enabling the app, or the UI will be blank.
npm install && npm run build
docker exec nextcloud php occ app:enable hermiq| Layer | Technology |
|---|---|
| Frontend | Vue 2.7, Pinia, @nextcloud/vue |
| Build | Webpack 5, @nextcloud/webpack-vue-config |
| Backend | PHP 8.1+, Nextcloud App Framework |
| Data | OpenRegister (PostgreSQL JSON objects) |
| UX | @conduction/nextcloud-vue |
| Quality | PHPCS, PHPMD, Psalm, PHPStan, ESLint, Stylelint |
| Branch | Purpose |
|---|---|
main |
Stable releases — triggers release workflow |
beta |
Beta / pre-release builds |
development |
Active development — merge target for feature branches |
The user-facing documentation site lives in docs/ — a Docusaurus site built on @conduction/docusaurus-preset with the brand <DetailHero> / <WidgetShelf> landing page, the journeydoc tutorial scaffold (docs/tutorials/ — user "first launch" + admin "manage settings"), and the Playwright docs-capture project for screenshots (hydra ADR-030).
.github/workflows/documentation.yml deploys the site on every push to development: it runs cd docs && npm ci && npm run build and publishes to <slug>.conduction.nl (hermiq's slug is hermiq, so hermiq.conduction.nl — docs/static/CNAME carries this). Build the site locally with:
cd docs
npm ci --legacy-peer-deps
npm run build # → build/ ([SUCCESS] Generated static files)
npm run start # local dev server with hot reloadProject / spec documentation:
| Resource | Description |
|---|---|
openspec/app-config.json |
App identity, goals, dependencies, and CI configuration |
openspec/specs/ |
Feature specs — what the app should do |
openspec/architecture/ |
App-specific Architectural Decision Records |
openspec/ROADMAP.md |
Product roadmap |
openspec/ |
Implementation specifications and changes |
- Accessibility: WCAG AA (Dutch government requirement)
- Authorization: RBAC via OpenRegister
- Audit trail: Full change history on all objects
- Localization: English and Dutch
- OpenRegister — Object storage layer (required dependency)
Add related apps here as integrations are built.
The js/ build output is not committed to the repo. Run the frontend build before enabling the app:
npm install && npm run buildFor support, contact us at support@conduction.nl.
For a Service Level Agreement (SLA), contact sales@conduction.nl.
This project is licensed under the EUPL-1.2.
All dependencies (PHP and JavaScript) are automatically checked against an approved license allowlist during CI. The following SPDX license families are approved:
- Permissive: MIT, ISC, BSD-2-Clause, BSD-3-Clause, 0BSD, Apache-2.0, Unlicense, CC0-1.0, CC-BY-3.0, CC-BY-4.0, Zlib, BlueOak-1.0.0, Artistic-2.0, BSL-1.0
- Copyleft (EUPL-compatible): LGPL-2.0/2.1/3.0, GPL-2.0/3.0, AGPL-3.0, EUPL-1.1/1.2, MPL-2.0
- Font licenses: OFL-1.0, OFL-1.1
Built by Conduction — open-source software for Dutch government and public sector organizations.