Simulators
Linear changelog
What changed in each Linear simulator release, newest first.
v0.0.6 — 2026-11-05
-
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. -
A paced clock keeps wall time, and
rystic run --speed 0stops it. UnderRYSTIC_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 runhands the twin only the pace it resolved, so--speed 0, a scenario without events,--tapeand--livekeep the clock manual even when your shell exportsRYSTIC_PACE. -
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. -
A malformed reference id is refused as Linear refuses it.
commentCreate,attachmentCreate,attachmentLinkURL,projectCreate,projectMilestoneCreateandprojectUpdateCreategiven anissueId,projectIdorteamIdsentry that is not a valid id now answerINVALID_INPUT“Argument Validation Error”, withuserPresentableMessageand avalidationErrorsentry naming the field, its value and the failed check. Before, they answeredGRAPHQL_VALIDATION_FAILED“Expected value of type …”. -
commentsanddocumentsrefuse a page size below 1 as Linear does.firstbelow 1 answersINVALID_INPUTwithuserPresentableMessage“first must not be less than 1.” and avalidationErrorsentry namingfirst, its value and the failedmincheck. Before, they answered “Argument Validation Error” with no detail. -
A badly typed variable or a misused directive is refused before anything runs. A variable whose value its declared type does not hold (
5for aString!, an unknown input field, a value outside an enum) now answers 400BAD_USER_INPUTwith 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, answersGRAPHQL_VALIDATION_FAILED. Before, the operation ran. -
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 anInt, a required input field left out or an unknown one) now answersGRAPHQL_VALIDATION_FAILEDwith graphql-js’s text, as anullfor a required argument already did. Before, the query or mutation ran with the value. -
issues,usersandworkflowStatesapply theirfilter. 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 byandandor, 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. -
issue(id:)takes an identifier,includeArchivedlists trashed issues, and a new attachment answers itssourceType.issue(id: "ENG-101")now resolves the issue as Linear does, not only its UUID.issues(includeArchived: true)now also lists issues deleted withissueDelete, each withtrashed: true.attachmentCreateandattachmentLinkURLnow answer and storesourceType: "unknown"where they answerednull, including a create on a url linked before. -
A state dump seeds back as it was, and the next record gets a fresh id.
GET /_rystic/inspectposted toPOST /_rystic/seedafter 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=sequenceslists every id counter, and a world document’s newsequenceskey restores them, so ids the simulator renders from a counter continue where the dump left off. -
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.
-
The call log shows what each call asked for and whether it succeeded. Each
GET /_rystic/callsentry now carriesargs, the call’s query and body arguments with credentials redacted (long values cut, file contents left out), andok, false for an answer that is not 2xx. A GraphQL answer carryingerrorsat HTTP 200 is not ok either, and its first error’s code (or, without one, its message) is the entry’serror. A path the simulator does not serve is markedunknown_route: true.?tail=Nreturns only the last N calls, anddroppedcounts the calls the log no longer keeps. -
The docs are one page, with a page per feature. The archive ships
README.mdin place ofHOWTO.md, with the same sections in the same order as every Rystic simulator’s page, andCHANGELOG.mdbeside it. Filters and the GraphiQL explorer each have their own page underdocs/features/, and every example on these pages runs as written against a fresh simulator. -
The docs page links the admin token section. The page now sends you to the admin token’s section of the shared-simulators page.
-
Scenario descriptions use customer terms. The Scenarios table and
--list-scenariosdescribe each scenario in American spelling, with no internal references. -
Answers carry Linear’s rate-limit and complexity headers, and a query over 10,000 points is refused. Every executed answer carries
x-complexityand thex-ratelimit-requests-*andx-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 onlyid0.2, and most connections multiply their selection byfirstorlast(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 400INPUT_ERROR, and spending past either budget answers 429RATELIMITEDwithRetry-After. Resetting the simulator fills both budgets. -
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
-
A world can carry its own timeline. A
--seedfile, or a complete world document posted toPOST /_rystic/seed, may addcredentials, binding presented credentials to actors, andevents: 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-levelcredentialsoreventskey the twin used to ignore now takes effect.RYSTIC_PACE=<speed>plays the clock, and the timeline, in real time, and an event’satmay be+<duration>./_rystic/inject,--injectandRYSTIC_INJECTnow needstate_setinstead ofscenarios. -
A timeline call can hand what it minted to later events. A
callevent may addbind,{"<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. -
linear_sprint_boardplays 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 throughPOST /graphqlunder an API key the world binds to that person; advance the clock to watch the sprint move. -
A clock jump fires each timeline event at its own time. When one
POST /_rystic/clockor paced tick passes several events, each fires with the clock at itsat, 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. -
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. -
A timeline call can carry raw bytes, XML or a JSON array. A
callevent may sendbody_textorbody_base64instead ofbody; 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 inbody_text. Abodymay 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
-
The scripts in this archive are MIT-licensed.
check.shand 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.txtno longer covers them; it still governs the twin itself. -
The docs say what an empty workspace’s
vieweranswers. Booted without--scenario, the twin answersviewerwith its placeholder user until you seed a person; the plans section said it was refused.
v0.0.3 — 2026-10-11
- 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/networkand/_rystic/ws-dropanswer 403 with"feature":"faults"and stay Pro and Enterprise.
v0.0.2 — 2026-10-11
-
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-000000000001and the first comment925e28d1-dad3-4fd4-8000-000000000001, where both were00000001-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. -
Introspection answers each argument’s type.
__type’sfields { args }now carrytypeas the specification nests it (kind,name,ofType), so a client that reads argument signatures, such asargs { name type { kind name ofType { kind name } } }, gets them instead of a validation error. -
Nested lists answer the records that name them. An issue’s
parent,children,cycle,project,labels,relationsandinverseRelations, a cycle’s, label’s and project’sissues, a project’steams, a team’sprojects, a user’sassignedIssues, and the organization’steamsandusersnow list what the workspace holds, where they answered empty or null. An issue filed with aparentIdorlabelIds, or updated to one, shows up under its parent’schildrenand each label’sissues, and a new issue’screatorreads the caller.issueCreatetakes the parent by UUID or by identifier such asENG-102;issueUpdatestill drops an identifier, so name the parent by its UUID there. -
vieweranswers in a world with no scenario, and a seed names its caller undermember. A twin started without a scenario resolves its caller, a placeholder user with an empty name, sovieweranswers a user instead of an error. A world or a seed names the personvieweranswers by keying thatstate_userrecordmember, itsidits own; every shipped world does. A seed that keys none gets the placeholder, whereviewerused to answer the user markedisMe. -
The guide lists what the twin serves and what it does not, from the model.
HOWTO.mdand 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 personvieweranswers: astate_userkeyedmember. -
A JSON field or a small payload object the twin does not model no longer fails the response.
reactionData,Team.securitySettingsandsearchIssues’archivePayloadanswer an empty value where they answeredCannot return null for non-nullable field …, so@linear/sdk’sissues(),issue(id),createIssue,updateIssue,createCommentandsearchIssuesreturn data.teams(),projects()andcreateProjectstill fail onTeam.visibilityandProject.status. -
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!whereString!is expected) 400GRAPHQL_VALIDATION_FAILED, a document with several operations and nooperationName400INPUT_ERROR, a subscription 400 with “Cannot query field … on type “Subscription"", and a JSON-array batch body 400BAD_REQUEST“Operation batching disabled.” Before, each one executed or answered a different error. -
attachment(id:)answers Linear’s not-found error. Asking for an attachment that does not exist now answersINPUT_ERRORwithstatusCode400, the messageEntity not found: Attachment: could not find by url.andCould not find referenced attachment with this url., as Linear does, instead of anINVALID_INPUT“Argument Validation Error”. -
A request without
Authorizationis refused before it runs. A GraphQL request with noAuthorizationheader now answers401with Linear’sAUTHENTICATION_ERRORerror before any field resolves, so a refused mutation writes nothing; it used to run as the world’s caller. An emptyAuthorizationheader 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 aBearertoken, is still accepted, so a test that sends no header now needs a placeholder key. -
Introspection answers each field’s type and an input object’s fields.
__type’sfieldsnow carrytype(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
-
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 athttp://localhost:8080/graphql(apiUrlin@linear/sdk) and your existing queries and mutations run against it unchanged. -
Five named workspaces, and the one your test needs.
--scenario linear_sprint_boardboots 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_inboxholds five untriaged support issues,linear_roadmap_quarterthree projects with milestones and status updates,linear_solo_workspaceone person and one team, andlinear_archive_vaultarchived and trashed records;SCENARIOS.mdsays what each is for. On Pro and Enterprise,POST /_rystic/seedlands teams, people, workflow states, cycles and any issue, comment, label, project or document directly.POST /_rystic/resetrewinds the ID sequence and the clock, so two identical runs give byte-identical issues down toid,identifier,numberandcreatedAt. -
It never reaches the network, and it needs no key. Any
Authorizationvalue is accepted, as a bare API key or aBearertoken, 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 answersGRAPHQL_VALIDATION_FAILEDrather than empty data. -
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.mdunder Limitations: some non-null fields (Issue.priorityLabel,Issue.branchName,Project.state) fail the whole response;issue.labelsandissue.attachmentsanswer empty; a new issue without astateIdhas no state; a new issue in a world is numbered from 1;issuesignoresfilter; and__schemaintrospection is refused. Each release states its own number.