Skip to Content
SimulatorsLinear changelog

Simulators

Linear changelog

What changed in each Linear simulator release, newest first.

v0.0.6 — 2026-11-05

  1. Setting the clock never stamps records before the timeline events it fired. When POST /_rystic/clock {"time": ...} fires timeline events due at its target, the next request’s records stamp after theirs instead of reusing their instant; a set past every event still stamps one millisecond past its target, as before.

  2. A paced clock keeps wall time, and rystic run --speed 0 stops it. Under RYSTIC_PACE, each tick moves the clock by the wall time that passed since the last, so a tick missed while the twin was busy no longer makes timeline events fire late. rystic run hands the twin only the pace it resolved, so --speed 0, a scenario without events, --tape and --live keep the clock manual even when your shell exports RYSTIC_PACE.

  3. GraphiQL to read and query the twin from the browser. /_rystic/graphiql, linked from the state UI header, is GraphiQL over Linear’s schema trimmed to the queries and mutations this twin serves; each query runs against the twin.

  4. A malformed reference id is refused as Linear refuses it. commentCreate, attachmentCreate, attachmentLinkURL, projectCreate, projectMilestoneCreate and projectUpdateCreate given an issueId, projectId or teamIds entry that is not a valid id now answer INVALID_INPUT “Argument Validation Error”, with userPresentableMessage and a validationErrors entry naming the field, its value and the failed check. Before, they answered GRAPHQL_VALIDATION_FAILED “Expected value of type …”.

  5. comments and documents refuse a page size below 1 as Linear does. first below 1 answers INVALID_INPUT with userPresentableMessage “first must not be less than 1.” and a validationErrors entry naming first, its value and the failed min check. Before, they answered “Argument Validation Error” with no detail.

  6. A badly typed variable or a misused directive is refused before anything runs. A variable whose value its declared type does not hold (5 for a String!, an unknown input field, a value outside an enum) now answers 400 BAD_USER_INPUT with graphql-js’s “Variable “$v” got invalid value …” text, the code Linear gives a missing variable. An unknown, misplaced or repeated directive, or one missing a required argument, answers GRAPHQL_VALIDATION_FAILED. Before, the operation ran.

  7. A GraphQL argument written with a value of the wrong type is refused before anything runs. A literal in the query that its argument’s declared type does not hold (an enum value outside the enum, "5" for an Int, a required input field left out or an unknown one) now answers GRAPHQL_VALIDATION_FAILED with graphql-js’s text, as a null for a required argument already did. Before, the query or mutation ran with the value.

  8. issues, users and workflowStates apply their filter. A filter on a list’s own fields (title, number, email, name, type, …) or one hop into a related record (team: { key: … }, state: { type: … }, assignee: { email: … }, labels: { name: … }), joined by and and or, now narrows the list as Linear does. Before, each list answered every record whatever the filter said. A filter field the twin does not read is refused rather than ignored.

  9. issue(id:) takes an identifier, includeArchived lists trashed issues, and a new attachment answers its sourceType. issue(id: "ENG-101") now resolves the issue as Linear does, not only its UUID. issues(includeArchived: true) now also lists issues deleted with issueDelete, each with trashed: true. attachmentCreate and attachmentLinkURL now answer and store sourceType: "unknown" where they answered null, including a create on a url linked before.

  10. A state dump seeds back as it was, and the next record gets a fresh id. GET /_rystic/inspect posted to POST /_rystic/seed after a reset restores every record where it was, per-caller records included, which used to land under a doubled key. The next record created after any seed passes every uuid and event id the simulator minted that the seed holds. GET /_rystic/inspect?view=sequences lists every id counter, and a world document’s new sequences key restores them, so ids the simulator renders from a counter continue where the dump left off.

  11. A slow client no longer holds up the others. The simulator reads a request’s whole body before it starts serving it and sends the answer after, so a client that uploads slowly, or reads a large answer slowly, no longer delays other clients’ requests. Answers are unchanged; one larger than 4 MiB still makes other requests wait while it is sent.

  12. The call log shows what each call asked for and whether it succeeded. Each GET /_rystic/calls entry now carries args, the call’s query and body arguments with credentials redacted (long values cut, file contents left out), and ok, false for an answer that is not 2xx. A GraphQL answer carrying errors at HTTP 200 is not ok either, and its first error’s code (or, without one, its message) is the entry’s error. A path the simulator does not serve is marked unknown_route: true. ?tail=N returns only the last N calls, and dropped counts the calls the log no longer keeps.

  13. The docs are one page, with a page per feature. The archive ships README.md in place of HOWTO.md, with the same sections in the same order as every Rystic simulator’s page, and CHANGELOG.md beside it. Filters and the GraphiQL explorer each have their own page under docs/features/, and every example on these pages runs as written against a fresh simulator.

  14. The docs page links the admin token section. The page now sends you to the admin token’s section of the shared-simulators page.

  15. Scenario descriptions use customer terms. The Scenarios table and --list-scenarios describe each scenario in American spelling, with no internal references.

  16. Answers carry Linear’s rate-limit and complexity headers, and a query over 10,000 points is refused. Every executed answer carries x-complexity and the x-ratelimit-requests-* and x-ratelimit-complexity-* headers, with each reset given in Unix milliseconds, so backoff code can be tested against the budgets it reads. A query’s complexity is priced as Linear prices it, before it runs: a property is 0.1, an object 1, an object selecting only id 0.2, and most connections multiply their selection by first or last (50 when neither is given). The two budgets match an API key’s: 2,500 requests and 3,000,000 points an hour, refilling continuously. A query over 10,000 points answers Linear’s 400 INPUT_ERROR, and spending past either budget answers 429 RATELIMITED with Retry-After. Resetting the simulator fills both budgets.

  17. Pro starts with a 14-day trial. Free has ended, so every simulator is on Pro and Enterprise, and the docs name only those plans.

v0.0.5 — 2026-10-12

  1. A world can carry its own timeline. A --seed file, or a complete world document posted to POST /_rystic/seed, may add credentials, binding presented credentials to actors, and events: calls the twin makes on its own API at set sim times as one of those actors, through the same handlers and egress as your requests. A top-level credentials or events key the twin used to ignore now takes effect. RYSTIC_PACE=<speed> plays the clock, and the timeline, in real time, and an event’s at may be +<duration>. /_rystic/inject, --inject and RYSTIC_INJECT now need state_set instead of scenarios.

  2. A timeline call can hand what it minted to later events. A call event may add bind, {"<name>": "<JSON Pointer>"}, saving fields of its answer, such as an ID the twin minted; a later event uses one as ${name} in a call’s path, headers or body or in a seed event’s record keys and values (a register name cannot be bound). A schedule naming a ${name} no earlier call binds is refused at load, and an event whose binding call failed records an error instead of firing.

  3. linear_sprint_board plays a timeline. Over the four hours after the pinned clock, Hana replies on ENG-103 and moves it to Done, Deploy Bot attaches a production deploy to it, Tomas starts ENG-102 and Yuki files a Backlog follow-up, each through POST /graphql under an API key the world binds to that person; advance the clock to watch the sprint move.

  4. A clock jump fires each timeline event at its own time. When one POST /_rystic/clock or paced tick passes several events, each fires with the clock at its at, so the records it creates carry that time, and the clock then lands where the move asked. An event already due when the clock moves fires at the current time.

  5. A clock jump stamps each timeline event at its exact instant. When a clock move passes timeline events, the records each one makes carry exactly its at, to the millisecond, whatever traffic came before it, so one jump leaves the same records as stepping the clock to each event in turn.

  6. A timeline call can carry raw bytes, XML or a JSON array. A call event may send body_text or body_base64 instead of body; the twin sends it as written, under the route’s own content type (XML, bytes, JSON or a form), so a scheduled call can upload an object’s bytes or post an XML document, and ${name} fills in body_text. A body may be a JSON array where the route reads one. A call that answers XML binds from its elements by JSON Pointer, such as /InitiateMultipartUploadResult/UploadId.

v0.0.4 — 2026-10-11

  1. The scripts in this archive are MIT-licensed. check.sh and the archive’s other scripts are published under the MIT License at https://github.com/Rystic-Inc/rystic-scripts , so you can copy, change and share them. LICENSE.txt no longer covers them; it still governs the twin itself.

  2. The docs say what an empty workspace’s viewer answers. Booted without --scenario, the twin answers viewer with its placeholder user until you seed a person; the plans section said it was refused.

v0.0.3 — 2026-10-11

  1. Free runs every control route but latency and fault injection. On Free, scenarios, seeding, the clock, inspect, the state UI and replay now work as on Pro, and the twin boots empty without --scenario. /_rystic/network and /_rystic/ws-drop answer 403 with "feature":"faults" and stay Pro and Enterprise.

v0.0.2 — 2026-10-11

  1. An issue and a comment never share an ID. Each kind of record the twin creates draws its IDs from its own range: the first issue you file is 554dd3a1-3e25-42f1-8000-000000000001 and the first comment 925e28d1-dad3-4fd4-8000-000000000001, where both were 00000001-0000-4000-8001-000000000001. Ids still count up in their last digits and restart on reset; a test that pinned an old ID needs the new one.

  2. Introspection answers each argument’s type. __type’s fields { args } now carry type as the specification nests it (kind, name, ofType), so a client that reads argument signatures, such as args { name type { kind name ofType { kind name } } }, gets them instead of a validation error.

  3. Nested lists answer the records that name them. An issue’s parent, children, cycle, project, labels, relations and inverseRelations, a cycle’s, label’s and project’s issues, a project’s teams, a team’s projects, a user’s assignedIssues, and the organization’s teams and users now list what the workspace holds, where they answered empty or null. An issue filed with a parentId or labelIds, or updated to one, shows up under its parent’s children and each label’s issues, and a new issue’s creator reads the caller. issueCreate takes the parent by UUID or by identifier such as ENG-102; issueUpdate still drops an identifier, so name the parent by its UUID there.

  4. viewer answers in a world with no scenario, and a seed names its caller under member. A twin started without a scenario resolves its caller, a placeholder user with an empty name, so viewer answers a user instead of an error. A world or a seed names the person viewer answers by keying that state_user record member, its id its own; every shipped world does. A seed that keys none gets the placeholder, where viewer used to answer the user marked isMe.

  5. The guide lists what the twin serves and what it does not, from the model. HOWTO.md and the docs page carry the served operations and the known gaps generated from the twin’s model and its open issues: the schema operations not served, the non-null fields the model does not declare, the branches the fidelity suite does not measure, what the model leaves out, and the known differences from Linear. They also say how to seed the person viewer answers: a state_user keyed member.

  6. A JSON field or a small payload object the twin does not model no longer fails the response. reactionData, Team.securitySettings and searchIssues’ archivePayload answer an empty value where they answered Cannot return null for non-nullable field …, so @linear/sdk’s issues(), issue(id), createIssue, updateIssue, createComment and searchIssues return data. teams(), projects() and createProject still fail on Team.visibility and Project.status.

  7. Malformed requests are refused before they run, as Linear refuses them. A missing required variable now answers 400 BAD_USER_INPUT, a variable whose type does not fit its argument ($id: ID! where String! is expected) 400 GRAPHQL_VALIDATION_FAILED, a document with several operations and no operationName 400 INPUT_ERROR, a subscription 400 with “Cannot query field … on type “Subscription"", and a JSON-array batch body 400 BAD_REQUEST “Operation batching disabled.” Before, each one executed or answered a different error.

  8. attachment(id:) answers Linear’s not-found error. Asking for an attachment that does not exist now answers INPUT_ERROR with statusCode 400, the message Entity not found: Attachment: could not find by url. and Could not find referenced attachment with this url., as Linear does, instead of an INVALID_INPUT “Argument Validation Error”.

  9. A request without Authorization is refused before it runs. A GraphQL request with no Authorization header now answers 401 with Linear’s AUTHENTICATION_ERROR error before any field resolves, so a refused mutation writes nothing; it used to run as the world’s caller. An empty Authorization header gets the same refusal, which is the twin’s own: Linear’s answer to an empty header is not measured. Any non-empty value, a bare key or a Bearer token, is still accepted, so a test that sends no header now needs a placeholder key.

  10. Introspection answers each field’s type and an input object’s fields. __type’s fields now carry type (kind, name, ofType), and __type(name: "IssueFilter") { inputFields { name type { kind name } } } lists an input object’s fields, instead of a validation error.

v0.0.1 — 2026-10-01

  1. A local Linear for your agent’s tests. The first release of the Linear twin: 58 GraphQL operations on Linear’s one endpoint, POST /graphql — issues (create, update, archive, unarchive, delete, search), comments, labels, relations, projects with milestones and status updates, documents and attachments, and read-only teams, users, workflow states, cycles and the organization — served from a model validated against the real Linear API. Point your client’s API URL at http://localhost:8080/graphql (apiUrl in @linear/sdk) and your existing queries and mutations run against it unchanged.

  2. Five named workspaces, and the one your test needs. --scenario linear_sprint_board boots an engineering team on the day cycle 7 closes — six people, six workflow states, three labels, eight issues with comments, relations and attachments — and is the world Free boots. linear_triage_inbox holds five untriaged support issues, linear_roadmap_quarter three projects with milestones and status updates, linear_solo_workspace one person and one team, and linear_archive_vault archived and trashed records; SCENARIOS.md says what each is for. On Pro and Enterprise, POST /_rystic/seed lands teams, people, workflow states, cycles and any issue, comment, label, project or document directly. POST /_rystic/reset rewinds the ID sequence and the clock, so two identical runs give byte-identical issues down to id, identifier, number and createdAt.

  3. It never reaches the network, and it needs no key. Any Authorization value is accepted, as a bare API key or a Bearer token, and the twin dials nothing: no test issue lands in a real workspace and no webhook fires. Not in this release, on purpose: workspace administration (creating teams, inviting users, workflow and cycle settings), integrations, OAuth applications, webhooks, billing and initiatives. A query for any of them answers GRAPHQL_VALIDATION_FAILED rather than empty data.

  4. What is measured, and what is not. Fidelity is scored differentially against the real Linear API, per response field, not by eyeball: the last full-suite round matched 64 of 71 probes on 2026-10-01, with 2 more inconclusive, on a 73-probe suite. That is an early number and this release ships it as-is. An issue read resolves its team, workflow state, assignee and comments, but driving the twin as an agent would found differences the score does not show, listed in HOWTO.md under Limitations: some non-null fields (Issue.priorityLabel, Issue.branchName, Project.state) fail the whole response; issue.labels and issue.attachments answer empty; a new issue without a stateId has no state; a new issue in a world is numbered from 1; issues ignores filter; and __schema introspection is refused. Each release states its own number.

Last updated on