Zulivio Documentation
Everything you need to install, configure, and operate Zulivio for your team. Scroll through — every section is on this one page.
Introduction
Zulivio
Open-source, self-hostable CRM and workforce-operations platform. One Docker install, no subscription, no per-seat fee, no data leaving your server.
What is Zulivio?
Zulivio is a role-based employee management, work-assignment, and attendance-tracking platform that runs entirely on infrastructure you control. It ships as a two-service Docker stack — a Next.js frontend and a NestJS backend backed by PostgreSQL — and is accessible from any device on your network once it’s running. There is no hosted version and no telemetry: the only outbound calls the app ever makes are ones you explicitly configure, like the optional Google Sheets sync.
It is currently the workforce-operations core of a much larger planned CRM. What’s shipped today — employees, assignments, attendance, knowledge base, dashboard, CSV/Sheets — is real and tested, not a preview. A full sales pipeline, marketing, and customer-service workspaces are on the public roadmap but not yet built; see Scope & Limitations for the exact line between the two.
Quick orientation
If this is your first time, follow these three steps:
- Install Zulivio — one command, a few minutes.
- Understand the role hierarchy — who can do what.
- Add your first employees — generates credentials automatically.
Key capabilities
- Role-based access control — Master Owner, Company Admin, Sales Head, Manager, Employee, enforced server-side on every request, never just a hidden button.
- Employee lifecycle — add, edit, reset password, and remove, all rank-guarded so no one can promote themselves or a peer.
- Work assignments — a guarded status pipeline with a full transition audit trail and outcome notes.
- Attendance tracking — an explicit shift/break state machine with server timestamps, not a self-reported timesheet.
- Knowledge base & tips — versioned PDF training with per-employee acknowledgement tracking, plus a daily team tip.
- Live master dashboard — headcount, assignment status, overdue work, and who’s on the clock right now.
- CSV & Google Sheets — import/export with formula-injection protection, and a real (credential-gated) Sheets adapter.
- Self-hosted — your data stays on your own server, in your own PostgreSQL database.
Browse the documentation
Getting Started
Using Zulivio
Administration
Architecture at a glance
Browser
│ http://<server>:3100 ← only exposed port
▼
┌─────────────────┐ /api/* proxied ┌──────────────────┐
│ Next.js :3100 │ ───────────────▶ │ NestJS :4100 │
│ (web) │ ◀─────────────── │ + Prisma │
└─────────────────┘ server-side └──────────┬───────┘
▼
PostgreSQL (Docker volume)The web service is the only container exposed on your network. The backend and database live entirely on the internal Docker network — neither is reachable from the LAN directly. All session cookies are first-party and httpOnly. See Architecture for the full picture.
License & attribution
Zulivio is released under the GNU Affero General Public License v3.0 (AGPL-3.0). It is free to use, self-host, fork, modify, and redistribute — including for commercial use. If you run a modified version as a network service for others, you must make that modified source available to those users under the same license. Built and maintained by NODEDR INFOTECH PRIVATE LIMITED.
Installation
Get Zulivio running on a server in a few minutes. The install script is the only prerequisite you need — it checks for everything else.
One-command install (recommended)
Clone the repository and run the install script. It checks for Docker, generates a .envfile with a random PostgreSQL password (only if one doesn’t already exist — safe to re-run), builds both images, starts the stack, waits for it to report healthy, and prints the URL to open.
git clone https://github.com/Raktim94/zulivio.git && cd zulivio && ./install.shRe-run ./install.sh any time — for example after git pull — to rebuild and restart. It never touches existing data.
Manual install
If you’d rather run the steps yourself:
cp .env.example .env
# Edit .env and set POSTGRES_PASSWORD to a real value — required,
# compose refuses to start without it.
# Generate one with: openssl rand -base64 32
docker compose up --buildFirst launch — creating your organization
Once the stack is healthy, open http://localhost:3100/setup and create your organization. This is a one-time step that creates your company and its first Master Owneraccount. There is no baked-in demo password — you choose the Master Owner’s password during setup.
BOOTSTRAP_DISABLED=true in .env and restart the backend service to permanently disable the /api/v1/bootstrap endpoint. Recommended for a single-tenant deployment so a second organization can never be created on the same instance.Changing the port
The default port is 3100. Edit HOST_PORT and FRONTEND_ORIGIN together in .env — docker-compose.yml reads this file automatically, so the compose file itself never needs editing:
# .env
HOST_PORT=4000
FRONTEND_ORIGIN=http://localhost:4000The two values must agree — FRONTEND_ORIGIN is used for CORS so the backend only accepts requests from the correct origin. Then restart: docker compose up -d.
Deploying to a VPS or cloud server
Zulivio runs the same way on a rented cloud server as it does on an office LAN box — the setup, the data, and the login are identical. The only difference is the values in .env:
# .env — publicly reachable host
HOST_PORT=3100
FRONTEND_ORIGIN=https://crm.yourdomain.comPut a reverse proxy with real HTTPS in front (Caddy, Nginx + Let’s Encrypt, or a managed proxy), point it at HOST_PORT, then run docker compose up -d.
Reaching it from outside your LAN
To make Zulivio reachable outside your office network without exposing a port directly, put it behind a tunnel — Cloudflare Tunnel, ngrok, Tailscale Funnel, or any reverse proxy you already run. None of these ship inside Zulivio’s own docker-compose.yml today; you run them alongside it, pointed at HOST_PORT. Once traffic reaches the app over https://, set your session cookie to Secure at the proxy/CORS layer to match — a plain http:// LAN setup should leave this off.
Accessing from other devices on the network
Any device on the same network — a second desktop, a manager’s laptop, a tablet — can open Zulivio by navigating to:
http://<IP-of-the-machine-running-Docker>:3100Find your machine’s LAN IP with ip addr (Linux), ipconfig (Windows), or System Preferences → Network (macOS). No per-device install — the browser is the client.
Stopping, restarting, and updating
# Stop containers (data is preserved in named volumes)
docker compose down
# Start again
docker compose up -d
# Stop AND delete all data (destructive — use only to start fresh)
docker compose down -v
# Update to a new version
git pull
docker compose up -d --buildOr re-run ./install.sh — it does exactly the same rebuild-and-restart, and never touches your data.
Roles & Access Control
A strict five-level hierarchy enforced on the server for every request — not a permission checkbox in the UI.
The hierarchy
| Role | Typical use | Can do |
|---|---|---|
| Master Owner | Company owner | Everything, including creating Company Admins |
| Company Admin | Operations lead | Everything except creating other admins/owners |
| Sales Head | Head of sales | Manage managers/employees, assignments, reports |
| Manager | Team lead | Add/remove employees below them, assign work, view team reports |
| Employee | Front-line staff | Own attendance, own assignments, knowledge base, tips |
How enforcement works
Every request is checked against this hierarchy in the backend — a NestJS guard plus per-service checks — never only by hiding a button in the interface. Concretely:
- A manager can never create, edit, or promote a peer or a higher role — privilege escalation is blocked on both create and edit, and covered by the automated test suite.
- An employee can never view another employee’s attendance or performance report.
- Every business record carries an
organizationId; every query is scoped to the organization from the authenticated session, never from client input — one Zulivio instance can safely host more than one organization’s data with no cross-tenant leakage.
This is tested, not just claimed: apps/backend/test/app.e2e-spec.ts runs 26 end-to-end tests against a real PostgreSQL database (not mocked), covering privilege escalation attempts, cross-employee report access, the full attendance state machine, and the full assignment lifecycle. All 26 pass at the last run.
Employee Management
Add, edit, and remove employees with auto-generated credentials — every action rank-guarded server-side.
Adding an employee
Creating an employee (Manager rank or above) automatically generates a unique employee number (EMP-0001, EMP-0002, …) and a random temporary password. The password is shown exactly once, at creation time, in the UI — it is never stored in plaintext and can never be retrieved again afterward.
Editing an employee
Update a subordinate’s role, department, or employment status — including reactivating someone SUSPENDED or ON_LEAVE — from the Employees page. Guarded the same way as creation: you can never promote someone to your own rank or above, even by accident.
Resetting a password
Force-generates a new temporary password for a subordinate and immediately revokes all of their active sessions — the reset takes effect right away, not on their next natural login.
Removing an employee
Removing an employee marks them SEPARATED and immediately revokes all their sessions. This is a soft action: their history is preserved for reporting and audit, there is no hard delete, and employee numbers are never reused.
Visibility scope
Manager rank and above have full visibility into every assignment, attendance record, and report in the organization. An Employee, or a Manager scoped to a specific team, sees only their own record or their direct reports’ — never the whole organization by default.
Work Assignments
Create work, assign it to a specific employee, and move it through a guarded status pipeline with a full audit trail.
Creating and assigning work
Create a piece of work and assign it to a selected employee by employee number. Reassignment works the same way — assign the same piece of work to a different employee number at any point in its lifecycle.
The status pipeline
Every assignment moves through an explicit, guarded pipeline:
ASSIGNED → IN_PROGRESS → FOLLOW_UP / BLOCKED → COMPLETED / CANCELEDTransitions are validated server-side. An invalid jump — for example, skipping straight from ASSIGNED to COMPLETED, or mutating an assignment that’s already in a terminal state — is rejected, not silently allowed.
Audit trail
Every status transition is recorded with who made it, when, and an optional outcome note, giving a full, tamper-evident history of how a piece of work moved from assigned to done — not just the current status.
Attendance Tracking
An explicit shift/break state machine with server timestamps — not a self-reported timesheet.
The state machine
logged_out → working → on_break → working → logged_outEvery transition is timestamped by the server, not the client. Only one open session is allowed per employee at a time — starting a second shift while one is already open is rejected. If an employee ends their shift while a break is still open, that dangling break is automatically closed rather than left hanging.
Employee reports
Every employee’s report shows:
- Login and logout times for every shift.
- Total worked minutes and total break minutes.
- A per-session breakdown, not just a daily total.
- Assignment counts by outcome — completed, follow-up, blocked, in-progress.
- Training acknowledgement status.
The same report is visible to the employee and to their manager — not a one-sided log the employee can’t see. There is no keylogging, no screenshot capture, and no webcam monitoring; attendance is measured by shift/break state, nothing else.
Knowledge Base & Tips
PDF training documents with a draft/publish lifecycle and per-version acknowledgement tracking, plus a daily team tip.
Uploading and publishing documents
Managers and above can upload a PDF training document. New uploads start as a draft— invisible to anyone else until explicitly published. Publishing makes it visible to whichever audience it’s assigned to.
Training assignments
Assign a published document to a role (e.g. every Employee) or to a specific person. Each employee’s acknowledgement is tracked per version — if a document is updated and republished, employees who acknowledged the previous version are asked to acknowledge the new one.
Today’s tips
A daily tip feed appears on every employee’s front page — a lightweight way to surface a reminder or a piece of process guidance without requiring a full document read.
Dashboard & Reports
One screen for the whole organization's operational health, updated live.
Master dashboard
The dashboard (GET /api/v1/reports/dashboard) surfaces:
- Current headcount.
- Assignments broken down by status.
- An overdue-assignment count.
- A live board of who’s currently working or on break.
- Knowledge-base stats — documents published, acknowledgement completion.
Per-employee reports
GET /api/v1/reports/employees/:employeeId combines the attendance report and assignment outcome counts for one employee into a single view — the same evidence a manager and the employee themselves both see.
Data Hub & Google Sheets
CSV import/export with formula-injection protection, plus a real, credential-gated Google Sheets adapter.
CSV export
Export the employee directory or the full assignment list as CSV at any time. Exports are sanitized against formula injection — a cell value starting with =, +, -, or @is neutralized before it reaches the file, so opening the export in Excel or Sheets can’t trigger an unexpected formula.
CSV import
Bulk-import employees from a CSV file (Manager rank or above). Errors are reported per row rather than failing the whole import — you get a clear list of which rows succeeded and which need fixing.
Google Sheets integration
The Sheets adapter is real, not a mock — it only activates once you provide credentials, per the project’s “no fake integrations” rule. Without credentials, GET /api/v1/integrations/google-sheets/status returns { configured: false } and the Data Hub page says so plainly — it never pretends the integration works.
To enable it:
- Create a Google Cloud service account and enable the Sheets API.
- Generate a JSON key and set
GOOGLE_SHEETS_CLIENT_EMAIL/GOOGLE_SHEETS_PRIVATE_KEYin.env— the private key needs its newlines escaped as\nin the.envfile. - Share your target spreadsheet with the service account’s email address.
- Restart the
backendservice. The Data Hub page shows “Connected” and exposes live import/export.
Environment Variables
The full list lives in .env.example — these are the ones that matter for day-to-day operation.
| Variable | Required | Purpose |
|---|---|---|
POSTGRES_PASSWORD | Yes | Shared by postgres/migrate/backend |
FRONTEND_ORIGIN | No | CORS allow-origin (default http://localhost:3100) |
HOST_PORT | No | Host port for the web app (default 3100) |
BOOTSTRAP_DISABLED | No | Set true to close self-service org creation |
GOOGLE_SHEETS_CLIENT_EMAIL / GOOGLE_SHEETS_PRIVATE_KEY | No | Enables live Sheets sync |
SEED_MASTER_OWNER_PASSWORD | Only for pnpm db:seed | Never baked into the image |
./install.sh generates POSTGRES_PASSWORD for you automatically on first run — you only need to touch this file for a non-default port, a public domain, or the optional Sheets integration.
Backup & Restore
All data lives in PostgreSQL, inside a named Docker volume — not inside the containers.
# Backup the database
docker compose exec postgres pg_dump -U nodedr zulivio | gzip > backup-$(date +%F).sql.gz
# Restore
gunzip -c backup-2026-08-12.sql.gz | docker compose exec -T postgres psql -U nodedr zulivio
# Run migrations manually (also run automatically by the migrate one-shot
# service on every startup)
docker compose exec backend npx prisma migrate deploy
# Tail logs
docker compose logs -f backend webdocker compose down -v deletes it, and that requires an explicit, separate command.CasaOS / ZimaOS
A CasaOS App Store manifest ships in the repository for home-server installs.
casaos/docker-compose.yml is the CasaOS manifest (x-casaos metadata, bind-mounted /DATA/AppData/$AppID/... volumes per CasaOS convention). It differs from the plain compose.yaml in two ways:
- Migrations run inline in the backend’s startup command (
prisma migrate deploy && node dist/src/main.js) instead of a separate one-shot service, since CasaOS doesn’t cleanly support init containers — safe becausemigrate deployis idempotent. - Images are referenced by tag (
ghcr.io/raktim94/zulivio-*) and built/published for bothamd64andarm64by a GitHub Actions workflow, rather than built locally.
The app icon and thumbnail are real, rendered from the actual in-app SVG lettermark — not placeholders. Official CasaOS/ZimaOS App Store listing submission is pending; today it installs via CasaOS’s own custom-install-from-URL flow, pointed at the manifest above.
Architecture
A two-service Docker stack: a Next.js frontend and a NestJS backend, talking over an internal network you never expose.
Stack
- Backend — NestJS 11 + Prisma 6 + PostgreSQL 16, session-based auth (Argon2id password hashing), REST API under
/api/v1. - Frontend — Next.js 15 (App Router) + React 19 + TanStack Query + Tailwind CSS v4. Talks to the backend only through a same-origin server-side proxy (
/api/*rewrites), so the session cookie stays first-party. - Monorepo — pnpm workspaces + Turborepo, Node.js 24.
- Shared types — a
packages/typesworkspace of TypeScript interfaces shared between the frontend and the backend API contracts.
Multi-tenancy
Every business record carries an immutable organizationId. Every repository query filters by the organization taken from the authenticated session — never from client input — so one instance can safely host more than one organization.
Authorization
A strict role hierarchy — EMPLOYEE < MANAGER < SALES_HEAD < COMPANY_ADMIN < MASTER_OWNER — enforced by a NestJS guard plus per-service checks (for example, an employee can only see their own attendance report).
File storage
Uploaded files (knowledge-base PDFs) live on local disk under UPLOADS_DIR — a named Docker volume in production — not as database blobs.
API Reference
The full REST surface under /api/v1, grouped by resource. Auth column: None = no session required, Session = any logged-in employee, Manager+ = Manager rank or above.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/health/live | None | Liveness probe — always returns ok if the process is up |
| GET | /api/health/ready | None | Readiness probe — fails if the database is unreachable |
| POST | /api/v1/bootstrap | None | Self-service organization creation — creates the company and its first Master Owner. Disabled when BOOTSTRAP_DISABLED=true |
| POST | /api/v1/auth/sessions | None | Log in with email + password; sets the httpOnly session cookie |
| POST | /api/v1/auth/sessions/logout | Session | Revoke the current session and clear the cookie |
| POST | /api/v1/auth/change-password | Session | Change the authenticated employee's own password |
| GET | /api/v1/employees | Session | List employees in the current organization |
| GET | /api/v1/employees/me | Session | Get the authenticated employee's own record |
| POST | /api/v1/employees | Manager+ | Create an employee — generates an employee number and a one-time temporary password |
| PATCH | /api/v1/employees/:id | Manager+ | Update role, department, or employment status. Rank-guarded — cannot promote to the actor's own rank or above |
| POST | /api/v1/employees/:id/reset-password | Manager+ | Force-generate a new temporary password for a subordinate and revoke their active sessions immediately |
| DELETE | /api/v1/employees/:id | Manager+ | Separate an employee and revoke their sessions. Soft — history is preserved, the employee number is never reused |
| GET | /api/v1/assignments | Session | List work assignments, scoped to what the actor is allowed to see |
| POST | /api/v1/assignments | Session | Create a new work assignment |
| POST | /api/v1/assignments/:id/assign | Session | Assign (or reassign) a piece of work to an employee by employee number |
| POST | /api/v1/assignments/:id/transitions | Session | Move an assignment through the status pipeline. Invalid transitions are rejected server-side |
| GET | /api/v1/work-sessions/me | Session | The authenticated employee's current shift/break state |
| GET | /api/v1/work-sessions/report/:employeeId | Session | Full attendance report for one employee — login/logout times, worked and break minutes, per-session breakdown |
| POST | /api/v1/work-sessions/start | Session | Start a shift. Rejected if the employee already has an open session |
| POST | /api/v1/work-sessions/:id/breaks/start | Session | Start a break on an open shift |
| POST | /api/v1/work-sessions/:id/breaks/:breakId/end | Session | End the current break and return to working |
| POST | /api/v1/work-sessions/:id/end | Session | End the shift. Auto-closes a dangling open break first |
| GET | /api/v1/knowledge/documents | Session | List knowledge-base documents visible to the actor |
| POST | /api/v1/knowledge/documents | Manager+ | Upload a PDF training document (starts as a draft) |
| POST | /api/v1/knowledge/documents/:id/publish | Manager+ | Publish a draft document, making it visible to its assigned audience |
| POST | /api/v1/knowledge/training-assignments | Manager+ | Assign a published document to a role or a specific person |
| GET | /api/v1/knowledge/training-assignments/me | Session | The authenticated employee's assigned training and acknowledgement status |
| POST | /api/v1/knowledge/training-assignments/:id/acknowledge | Session | Acknowledge a training assignment for its current published version |
| GET | /api/v1/tips/feed | Session | Today's tip feed for the employee front page |
| POST | /api/v1/tips | Manager+ | Create a new team tip |
| POST | /api/v1/tips/:id/acknowledge | Session | Mark a tip as seen/acknowledged |
| GET | /api/v1/reports/dashboard | Session | Master dashboard data — headcount, assignments by status, overdue count, live who's-working board |
| GET | /api/v1/reports/employees/:employeeId | Session | Combined attendance + assignment report for one employee |
| GET | /api/v1/exports/employees.csv | Session | Export the employee directory as CSV |
| GET | /api/v1/exports/assignments.csv | Session | Export work assignments as CSV |
| POST | /api/v1/imports/employees/csv | Manager+ | Bulk-import employees from a CSV file, with row-level error reporting |
| GET | /api/v1/integrations/google-sheets/status | Session | Whether the Google Sheets adapter is configured ({configured: false} if no credentials are set) |
| POST | /api/v1/integrations/google-sheets/export | Session | Push data to the connected Google Sheet |
| POST | /api/v1/integrations/google-sheets/import | Session | Pull data from the connected Google Sheet |
Security Model
Session-based auth, server-side authorization, and an audit trail — no client-trusted state.
Password hashing
Passwords are hashed with Argon2id, the current OWASP-recommended algorithm for password storage.
Sessions
Sessions use random 256-bit tokens; only the SHA-256 hash of the token is stored server-side, never the raw value. Cookies are httpOnly and SameSite=lax with a 12-hour TTL, and are revoked on logout, on password change, and immediately when an employee is removed or has their password reset.
Audit trail
An audit_events table records who did what to what, and when — deliberately excluding plaintext temporary passwords and password hashes from its metadata, so the audit log itself can never leak a credential.
Authorization
Every privilege-sensitive action is re-checked server-side against the actor’s role and the target’s rank — a Manager literally cannot construct a request that promotes a peer, regardless of what the UI shows them. This is covered by the automated e2e test suite (see Local Development for how to run it).
No row-level security in Postgres
Tenant isolation (multi-organization separation) is enforced entirely in the application layer today, not via Postgres row-level security policies. Every query is scoped by organizationId taken from the session in application code.
Local Development
Run the frontend and backend directly with pnpm, against a local PostgreSQL instance, without Docker.
pnpm install
# Start a local Postgres however you like, then:
cd apps/backend
DATABASE_URL="postgresql://user:pass@localhost:5432/zulivio" npx prisma migrate dev
DATABASE_URL="postgresql://user:pass@localhost:5432/zulivio" pnpm dev # backend on :4100
cd ../web
BACKEND_URL="http://localhost:4100" pnpm dev # frontend on :3100Running the test suite
cd apps/backend
pnpm typecheck # tsc --noEmit
pnpm build # nest build
# e2e/integration suite against a real Postgres (not mocked)
docker run --rm -d --name zulivio-test-pg -e POSTGRES_PASSWORD=test \
-e POSTGRES_DB=zulivio_test -p 55432:5432 postgres:16-alpine
DATABASE_URL="postgresql://postgres:test@localhost:55432/zulivio_test" \
npx prisma migrate deploy
DATABASE_URL="postgresql://postgres:test@localhost:55432/zulivio_test" \
NODE_ENV=test npx jest --config ./test/jest-e2e.json --runInBand
docker rm -f zulivio-test-pgThe suite covers bootstrap/login/logout, privilege escalation blocked on both create and edit, cross-employee report access blocked, owner edit/reset-password/remove on subordinates, the employee directory scoped to strictly-below-your-rank at three rank levels (never a peer or higher), the audit log gated to the Master Owner, the full attendance state machine (including rejecting a second concurrent session/break), and the full assignment lifecycle (including rejecting invalid transitions and mutations on a terminal state). 41/41 passing at the last run.
Scope & Limitations
What's intentionally not built yet — stated plainly rather than silently faked.
This build implements the workforce-operations core of a much larger specification, not the full spec. Explicitly not built yet:
- No leads/contacts/accounts/opportunities/pipeline CRM objects — this is an employee/assignment/attendance system, not (yet) a sales pipeline CRM.
- No background job queue or Redis — CSV import and PDF upload run synchronously in the request. Fine at small-team scale; a very large CSV import or PDF library would need this added.
- No S3-compatible object storage (MinIO) — files live on a local disk volume. Fine for a single-server deployment; not horizontally scalable as-is.
- No automation/workflow-rule engine, and no AI features.
- No MFA/SSO/OIDC — session + password only.
- No WhatsApp/telephony/email/calendar adapters.
- No row-level security (RLS) in Postgres — tenant isolation is enforced entirely in the application layer today.
- Assignment/employee sequence numbers are computed via
count() + 1inside a transaction; the database’s unique constraint prevents a collision from corrupting data, but under heavy concurrent writes a request could need a retry rather than silently succeeding. Fine at normal team-scale write volume.
None of the above are silently faked — where a feature isn’t built, there is no button or endpoint pretending it works.
Roadmap & Contributing
What's shipped today is the foundation of a much larger, publicly-planned product.
Roadmap
What’s in the repository today is the workforce-operations foundation of a larger product: a shared identity/permission/relationship core with purpose-built departmental workspaces (Sales, Marketing, Service, Success, Delivery, Field Service, People, Partner/Vendor) layered on top over time — integrations-first rather than rebuilding accounting, payroll, or telephony in-house, and governed AI added only after the data foundation is solid.
The full multi-month, phase-by-phase implementation plan — department workspace designs, feature catalogue, migration/rollout playbook, and go/no-go checklists — lives in ROADMAP.md on GitHub, and is summarized on the homepage roadmap section.
Contributing
Zulivio is AGPL-3.0-only — see LICENSE. Bug reports and pull requests are welcome on GitHub:
Before opening a pull request, run the typecheck, build, and e2e suite described in Local Development — the same checks CI runs.