Elyra
Elyra The coding agent eTerm The terminal that knows where each command ends Starf An activity monitor for Apple silicon that never invents a number e The native code editor Elyra Grove Native local development environment Askr The real server for Laravel & PHP Elyra Framework Rust + Svelte 5 framework for desktop apps Elyra Conductor Local project conductor Elyra SQL Server MySQL-compatible SQL server in Rust Elyra Félagi Agents as teammates on one board Elyra SQL Client Native desktop SQL workbench Elyra SQL Anywhere Replication-ready SQL engine Elyra Sjá SEO & GEO workspace for macOS Elyra DataGrid Server-driven data grid for Laravel
Start here
Concepts
Release notes
What's new
Elyra

Data model

Fifteen tables. The shape that matters most is the polymorphic actor, which appears in five of them.


The polymorphic actor

Anywhere the application records who did this, the columns are a type and an id:

Table Columns
issues assignee_type + assignee_id, creator_type + creator_id
whiteboards / whiteboard_elements One row per thing on a surface. visibility private or workspace; issue_id is the wire back to the data model
article_revisions What a page said before each edit. The text is a file beside the document, in {ulid}.history/
articles Knowledge base. parent_id self-referencing, path to the document on disk, search_text a plain-text cache of it, state deciding both who may read it and whether it is in the tree
timers One running stopwatch per person, user_id unique. Server-side so it survives a closed tab
time_entries actor_type + actor_id — an agent's hour is an hour
comments author_type + author_id
activities actor_type + actor_id (nullable — the system has no id)
projects lead_type + lead_id
tasks initiator_type + initiator_id

A morph map keeps user and agent in the database rather than fully qualified class names, so the columns stay readable and a namespace refactor does not rewrite data:

Relation::enforceMorphMap(['user' => User::class, 'agent' => Agent::class]);

This is the design the whole product rests on. It is why an agent can be assigned work, create issues, comment and appear in the audit log through exactly the same API as a person — and why there is no "run AI" button anywhere in the interface.


Tables

Identity and tenancy

Table Notable columns
users is_admin, email_verified_at, seen_version, email_notifications, two_factor_*, identity_provider_id + external_subject (unique — matched on the provider's sub, never on email) — the first two guarded
workspace_invitations email, role, token_hash, expires_at, accepted_at
workspaces slug, issue_prefix, issue_counter, context, repositories
workspace_members role (owner, admin, developer, member, management — a string, not an enum column, so a new role needs no migration), weekly_hours
identity_providers issuer, client_id, client_secret (encrypted), domains, enforced, satisfies_two_factor — platform-wide, because an account belongs to a person rather than a workspace
ip_allowlist_entries label, range, last_matched_at — platform-wide, not per workspace
daemon_tokens token_hash, token_prefix, last_used_at, revoked_at

Work

Table Notable columns
projects status, priority, lead_*, enabled_issue_types. No target_date: a project is a system and does not finish — dates belong to epics
issues cycle_id (nullOnDelete — deleting a window must not delete the work), carried_over (how many closed windows it has survived), source + external_key (unique per workspace — a second import updates), number, type, status, priority, parent_id, project_id, position, start_date, due_date, estimate_minutes, spent_minutes, nightwatch_issue_id (unique per workspace — their grouping, so a reopen finds the issue that exists rather than filing a second), closed_at (not fillable — maintained by the observer on every status change, because a column a form could write is one somebody can use to hide an issue from every board)
comments parent_id, source_task_id, body
activities action, properties, task_id, issue_id
whiteboards / whiteboard_elements One row per thing on a surface. visibility private or workspace; issue_id is the wire back to the data model
article_revisions What a page said before each edit. The text is a file beside the document, in {ulid}.history/
articles Knowledge base. parent_id self-referencing, path to the document on disk, search_text a plain-text cache of it, state deciding both who may read it and whether it is in the tree
timers One running stopwatch per person, user_id unique. Server-side so it survives a closed tab
time_entries actor_*, minutes, spent_on, source, task_id
issue_subscriber user_id, source — including opted_out
inbox_items type, summary, actor_*, read_at, archived_at
issue_dependencies predecessor_id, successor_id, type, lag_days
time_entries issue_id, meeting_id and absenceexactly one is set. Work has an issue or a meeting and no kind; absence has a kind and neither, so the two are mutually exclusive by construction rather than by a rule somebody has to remember
milestones issue_id (an epic, checked at every door rather than in the schema — a foreign key cannot say "only when this row's type is Epic"), workspace_id (not redundant: every report scopes by workspace first), name, target_on, status (planned, at_risk, reachedmissed is derived, never stored), reached_on, note, created_by
imports source, filename, column_map, value_map, counts, warnings
webhooks url, secret, events, consecutive_failures, disabled_at
attachments attachable_type + attachable_id (morph, so nothing cascades — deletion is explicit in the model), path never derived from the uploaded name, checksum
whiteboards / whiteboard_elements One row per thing on a surface. visibility private or workspace; issue_id is the wire back to the data model
article_revisions What a page said before each edit. The text is a file beside the document, in {ulid}.history/
articles Knowledge base. parent_id self-referencing, path to the document on disk, search_text a plain-text cache of it, state deciding both who may read it and whether it is in the tree
timers One running stopwatch per person, user_id unique. Server-side so it survives a closed tab
time_entries minutes, spent_on, actor morph, source — the truth under issues.spent_minutes, which is a cached sum. issue_id and meeting_id are both nullable and exactly one is set: an hour belongs to an issue or to a meeting
cycle_days One reading per cycle per day. Never recalculated, so a burndown cannot be redrawn by a later estimate
meeting_series The recurrence pattern, kept apart from the occurrences so a meeting is always one meeting
meetings Agenda, notes and minutes per project. minutes_sent_at is both the record of circulation and the guard against a second one. all_day changes the wording, not the arithmetic — the length is still stored in duration_minutes
meeting_attendees People, not actors — an agent does not attend a meeting
cycles number, starts_on, ends_on, closed_at — generated, never created by hand
labels / issue_label name unique per workspace, color a token name rather than a hex
artifacts type, state, url, url_hash, reference, title, task_id (nullable), issue_id. claim_check holds where the run's summary and its diff disagreed; claim_checked_at exists so "checked, nothing to report" and "never checked" are distinguishable

Execution

Table Notable columns
agents provider, runtime_id, instructions, custom_env, custom_args, mcp_config, max_concurrent_tasks, visibility, archived_at
runtimes ulid, daemon_id, provider, last_seen_at, client_capabilities
tasks ulid, status, session_id, work_dir, lease_expires_at, attempts, cancel_requested_at
task_messages seq, type, tool, content, payload

Knowledge

Table Notable columns
skill_proposals A correction that changed a run's outcome, offered as a skill. Unique on the comment, so a lesson is suggested once
skills slug, content
skill_files path, content
agent_skill enabled. What an agent has today
skill_task What a run was given, written when the work left for the runtime. Deliberately not derived from agent_skill: measuring whether a skill still earns its place means finding attachments that should change, and inferring past runs from present attachments answers using the thing under review
silent_actions Interactions that changed nothing and said nothing, aggregated by component and method rather than logged per occurrence. workspace_id is nullable, because a dead button on the password gate is exactly the kind that strands somebody

Integrations

Table Notable columns
nightwatch_hooks One per workspace. token is in the URL, secret is encrypted and never sent. default_project_id is where an application Félagi has never seen puts its first problem
nightwatch_applications One Nightwatch application, its project and the developer who picks its problems up. Learned on first sight, not configured in advance
github_hooks token, secret, active, app_id, private_key, installation_id, last_received_at, last_event, last_error, last_reported_at, last_report_error
subscribers email, name, token, unsubscribed_at
subscriptions subscriber_id, subscribable_type, subscribable_id

Automation and reporting

Table Notable columns
autopilots trigger, cron_expression, timezone, next_run_at, webhook_token_hash, issue_title
autopilot_runs trigger, status, reason, payload, issue_id, task_id
saved_views surface, name, filters, visibility
scenarios name, visibility, status, applied_at, applied_by
scenario_proposals subject_*, field, value, baseline

Decisions worth knowing

Issue numbers are reserved under a row lock. Workspace::reserveIssueNumber() locks the workspace row inside a transaction, so two people creating issues at the same instant cannot claim ACME-42 twice.

Positions are fractional. Dropping a card between two others sets its position to the midpoint, so a move never renumbers a column.

Public identifiers are ULIDs. runtimes and tasks carry a ulid beside their auto-increment key. Daemon endpoints address the ULID, so internal ids never leave the server.

Deletions preserve work. Deleting a project nulls issues.project_id; deleting an epic nulls its children's parent_id; removing a member deletes the membership, not the account. Agents are archived rather than deleted, because their name appears on history that must keep making sense.

spent_minutes is a cache, not a source. The truth is one row per piece of work in time_entries, each with an actor, a date and a source. A single integer on the issue can be shown and cannot answer who or when, which is the only thing a time report ever asks. The cache exists so an issue page costs one query.

No column may be named after an Eloquent internal. PHP lets a sibling subclass read another instance's protected members directly, so a column called original, previous or changes is read out of the framework's dirty-tracking arrays instead of the database — silently, and only from inside another model. A test walks every model's table and fails on any of them.

Sequence numbers make the output stream idempotent. task_messages is unique on (task_id, seq), so a daemon retrying after a network failure cannot double a line.

JSON columns have no database defaults. MySQL 8 refuses them. Defaults live in each model's $attributes, which also means a freshly created model answers questions about itself correctly before it has been read back — exactly when an observer asks.


Why artifacts are a table

They could have been a key inside tasks.result, which is already a JSON column and already free-form. One requirement settles it: the return path looks an artifact up by URL. A webhook saying "this pull request merged" arrives with nothing but a URL and a state, and has to find the issue it belongs to. A JSON column cannot be indexed for that question.

url_hash exists because the column it summarises cannot carry an index either: MySQL caps an index key at 3072 bytes, and a utf8mb4 varchar(2048) is 8192. The hash is of a normalised URL — scheme and host lowercased, trailing slash removed — so the same pull request written two ways still matches.

issue_id is denormalised from the task. An artifact is read from the issue far more often than from the run that produced it, and the issue page should not walk every task to find its own links.