Skip to content

feat!: generate all output models from the OpenAPI spec - #985

Draft
vdusek wants to merge 14 commits into
v3from
feat/openapi-generated-models
Draft

feat!: generate all output models from the OpenAPI spec#985
vdusek wants to merge 14 commits into
v3from
feat/openapi-generated-models

Conversation

@vdusek

@vdusek vdusek commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Generates every output model from the committed OpenAPI snapshot via openapi-typescript instead of
hand-writing it, and adds a scheduled job that keeps the snapshot fresh.

Important

Base needs retargeting. This branch sits on feat/openapi-spec-snapshot, which is not pushed
yet, so the first commit here (chore: vendor the Apify API OpenAPI spec and add a refresh script)
belongs to that PR. Review from the second commit onward.

How it works

pnpm generate:types turns src/generated/openapi.json into src/generated/api.ts. Nothing
re-exports that file -- src/models.ts declares each published model on top of a generated schema, and
src/spec_guards.ts asserts every deviation at compile time, so an invalidating spec change fails
pnpm build:node. One commit per resource group.

Published types that were wrong

Seven, four of them contradicting the client's own runtime:

  • nextExclusiveStartKey was a required string; listKeys() has always compared it to null.
  • Webhook.lastDispatch was string; the API returns an object.
  • Schedule.nextRunAt, Schedule.lastRunAt and RequestQueueClientRequestSchema.handledAt were
    string, but parseDateFields() had already converted them to Date.
  • MonthlyUsage.dailyServiceUsages[].date was Date but never converted -- it does not end in At.
  • Build.status omitted READY and RUNNING, which waitForFinish() documents.
  • RequestQueueClientGetRequestResult was a queue-head projection; the endpoint returns the whole request.
  • UserPlan.enabledPlatformFeatures used an enum missing three features that appear in
    EffectivePlatformFeatures. Now the spec's string[].

Where the spec is deliberately not adopted

Argued at each declaration, excluded from the width guard: ActorVersion keeps its discriminated union
(the flat spec shape leaves every variant unreachable), WebhookCondition keeps its single-id variants
(the flat shape would allow none or all three ids), ActorRun.generalAccess keeps RUN_GENERAL_ACCESS
(the spec reuses storage-wide GeneralAccess, which lists ANYONE_WITH_NAME_CAN_READ; a run has no
name). Schedule.timezone stays the curated IANA union.

Notes

  • Renovate cannot see the spec, so without the scheduled job the snapshot never moves and no guard can
    fire. A byte comparison would open a weekly noise PR -- info.version is an apify-docs build stamp,
    not an API version -- so isOnlyBuildStampChange() ignores that one field. Not automerged.
  • Types only; the browser bundle grows 4,264 B (+0.45%), almost all of it the four runtime enums moved
    into models.ts.
  • pnpm generate:types reproduces the committed api.ts byte for byte, enforced by the new
    generated_types job from the snapshot, never from docs.apify.com. Each guard rule was verified
    negatively by breaking it on purpose.

Open questions

  • The ActorVersion union vs. the spec's flat shape is the one decision that changes public API
    ergonomics rather than just accuracy.
  • RequestQueueClientBatchRequestsOperationResult still types the batch delete path, where the API
    answers with BatchDeleteResult. Fixing it needs a new published type and a changed return type.

✍️ Drafted by Claude Code

vdusek added 13 commits July 30, 2026 15:57
Lands only the input artifact. Nothing reads the snapshot yet -- the type generator, the
generated types and the scheduled sync workflow follow separately, so that change can land
as one complete functional unit.

The path is src/generated/ even though nothing generates the file today; it is where the
generator will look for it, and matching it now avoids a rename later.

scripts/fetch-spec.mts writes the response body verbatim instead of re-serializing it. The
endpoint already pretty-prints with a 2-space indent and LF, so passing the bytes through
keeps refreshes free of formatting-only diffs. It validates the body -- parseable JSON,
openapi 3.1.x, non-empty paths and components.schemas -- before touching the target, and
writes through a temp file plus rename, so a docs outage serving an error page with a 200
cannot land as a corrupt snapshot.

.gitattributes pins LF for the whole repo. All 156 tracked text files are already LF in the
index, so it renormalizes nothing; it exists so a clone made with core.autocrlf=true cannot
turn a refresh into 29k lines of line-ending churn.

tsconfig.scripts.json needs an explicit `types: ["node"]`. This setup gets no automatic
@types inclusion, and unlike src/ a script importing only node: builtins pulls in nothing
transitively, so without it the Node globals go unresolved.
Adds openapi-typescript as a dev dependency and generates src/generated/api.ts from the
committed spec snapshot, then adapts the Dataset and WebhookDispatch models on top of it.
Types only: nothing lands in the runtime.

The generated file is api.ts rather than api.d.ts on purpose. tsc does not copy .d.ts inputs
into outDir, so a declaration file would leave every published type referencing a path that
does not exist in dist.

`additionalProperties` is deliberately off. It is often taken for the analogue of the Python
client's `extra_fields = "allow"`, but that setting relaxes runtime validation while this flag
relaxes static typing, appending an index signature to every object. Responses are never
validated at runtime here, so unknown server fields already pass through untouched and the
flag would only make every property typo type-check.

Models are declared with `interface ... extends`, never as type aliases: the docs plugin only
emits API-reference pages for classes, interfaces and enums, so an alias silently deletes a
model's page.

src/spec_guards.ts turns spec drift into a build failure. It is not re-exported from
src/index.ts, so its exports satisfy noUnusedLocals without growing the public API or the
rendered reference. Covered: a field an override block replaces being dropped or renamed, a
documented spec gap being filled, and the shared @apify/consts enums diverging from the spec.

Scoping the js-yaml override in pnpm-workspace.yaml is a prerequisite, not a drive-by. The v4
line was raised to ^5.0.0 as a routine major in #947, having started as a ^4.2.0 advisory
floor in #942; js-yaml 5 dropped the `types` export that @redocly/openapi-core reads at import
time, so openapi-typescript cannot run in this repo without the exemption.

The generator's pure helpers are unit tested from test/spec_transform.test.mts. The `.mts`
extension is required, not cosmetic: a CommonJS test cannot `require` the ESM script it
imports, so vitest's include glob gained `mts` alongside `js` and `ts`.

BREAKING CHANGE: Dataset, DatasetStats, DatasetStatistics, FieldStatistics, WebhookDispatch,
WebhookDispatchCall and WebhookDispatchEventData now follow the spec's nullability and
optionality instead of the previous hand-written shapes. Dataset.name, Dataset.actId and
Dataset.actRunId gain `| null`; Dataset.fields becomes optional and nullable; Dataset.stats
and Dataset.itemsPublicUrl become optional; DatasetStatistics.fieldStatistics becomes optional
and nullable; FieldStatistics.min, max, nullCount and emptyCount gain `| null`;
WebhookDispatch.calls and WebhookDispatch.eventData become optional; and
WebhookDispatch.webhook changes from Pick<Webhook, 'requestUrl' | 'isAdHoc'> to
WebhookDispatchWebhookSummary, which is nullable and also carries actionType and condition.
Newly exposed: Dataset.consoleUrl, Dataset.schema and DatasetStats.inflatedBytes.
Adopting the spec fixes a type lie that `listKeys()` already worked around at runtime:
`nextExclusiveStartKey` was published as a required `string`, while the pagination loop has
always compared it against `null` and the API omits it entirely for a listing that was not
truncated. The loop's check widens from `!== null` to `!= null` to match.

`title` is the only field of the store that the spec still does not describe, so it is the
whole of KeyValueStoreSpecGaps; `username` moved out of the gap list because the spec covers
it now. `generalAccess` keeps its `| null`, which the spec omits, for the same reason as on
Dataset -- a storage may follow the owner's user setting.

BREAKING CHANGE: KeyValueStore, KeyValueStoreStats, KeyValueListItem and
KeyValueClientListKeysResult now follow the spec's nullability and optionality instead of the
previous hand-written shapes. KeyValueStore.name, actId, actRunId and username gain `| null`;
KeyValueStore.userId becomes optional and nullable; KeyValueStore.keysPublicUrl becomes
optional; KeyValueClientListKeysResult.exclusiveStartKey and nextExclusiveStartKey become
optional and nullable. Newly exposed: KeyValueStore.consoleUrl, KeyValueStore.recordsPublicUrl,
KeyValueStore.schema and KeyValueStoreStats.s3StorageBytes.
The spec models a version as a single flat object with all four source locations optional and
`sourceType` nullable. The published discriminated union is kept regardless, because it narrows
the source location down to the one field that actually applies -- so the four locations are
dropped from the shared base and reinstated, as required, by the variant whose `sourceType`
implies them. Dropping the spec's `sourceType: null` is the one deliberate narrowing here: a
version with no source type carries no usable source location either, so adopting the `null`
would make every variant unreachable and buy nothing.

`ActorSourceType` gains `SOURCE_CODE`. Both the spec and `@apify/consts` list it, so a version
built that way was previously unrepresentable; `ActorVersionSourceCode` is the matching variant
and adds no source location of its own. `spec_guards.ts` now pins the enum from both sides, the
same way `WebhookDispatchStatus` already was.

The `sourceFiles` element type also gains a folder shape. `SourceCodeFolder` was always part of
that list -- the API distinguishes folders with a `folder` flag rather than by nesting -- but the
hand-written `ActorVersionSourceFile` could not express one.

BREAKING CHANGE: ActorVersion, its four source-type variants, ActorVersionSourceFile,
ActorEnvironmentVariable and FinalActorVersion now follow the spec's nullability and
optionality. BaseActorVersion.versionNumber becomes required; buildTag, applyEnvVarsToBuild and
envVars gain `| null`; ActorVersionSourceFile.format and content become optional, and its
`format` is the spec's `SourceCodeFileFormat` rather than an inline `'TEXT' | 'BASE64'`;
ActorEnvironmentVariable.name becomes required and isSecret gains `| null`;
ActorVersionSourceFiles.sourceFiles accepts ActorVersionSourceFolder entries as well; and
FinalActorVersion.buildTag is `string | null` rather than `string`. Newly added:
ActorSourceType.SourceCode, ActorVersionSourceCode and ActorVersionSourceFolder.
The pricing union transfers wholesale. The spec models it as a `oneOf` with a `pricingModel`
discriminator and an OpenAPI 3.1 `const` per variant, so `openapi-typescript` emits literal
types and the published union keeps narrowing exactly as before -- no override needed. That also
brings tiered pricing in: `PricePerDatasetItemActorPricingInfo.tieredPricing` and
`ActorChargeEvent.eventTieredPricingUsd`, which overlaps what #970 adds by hand against master.

`ActorDefinition.output` is the one field of the definition the spec still omits, so it is the
whole of ActorDefinitionSpecGaps. Its `readme`, `input` and `changelog` keep the `| null` the
spec drops -- they are paths into the Actor's source tree and the API reports `null` for a file
that is absent, which is what the hand-written definition already recorded.

`Actor.versions` is excluded from the width guard on purpose. It re-points at the discriminated
`ActorVersion` union, which is deliberately narrower than the spec's flat `Version`, so the
one-directional assignability rule does not apply to it.

BREAKING CHANGE: Actor, ActorStats, ActorStandby, ActorDefaultRunOptions, ActorExampleRunInput,
ActorTaggedBuild, ActorTaggedBuilds, ActorDefinition, ActorChargeEvent, ActorCollectionListItem
and the four pricing-info shapes now follow the spec's nullability and optionality.
Actor.actorStandby loses the `& { isEnabled: boolean }` intersection and gains `| null`;
Actor.deploymentKey and actorPermissionLevel become optional; Actor.description, title,
seoTitle, seoDescription, isDeprecated, exampleRunInput and taggedBuilds gain `| null`; every
field of ActorStats becomes optional; every field of ActorDefaultRunOptions becomes optional;
ActorExampleRunInput.body and contentType become optional; ActorTaggedBuilds values may be
`null`; ActorDefinition.actorSpecification, name and version become optional;
ActorChargeEvent.eventDescription becomes required while eventPriceUsd becomes optional;
FlatPricePerMonthActorPricingInfo.trialMinutes and
PricePerDatasetItemActorPricingInfo.unitName become required, while the latter's
pricePerUnitUsd becomes optional. Newly exposed: Actor.pictureUrl, standbyUrl, notice,
isCritical, isGeneric, isSourceCodeHidden and hasNoDataset; ActorStats.actorReviewCount,
actorReviewRating, bookmarkCount and publicActorRunStats30Days;
ActorDefaultRunOptions.maxItems and forcePermissionLevel; ActorDefinition.defaultMemoryMbytes;
ActorTaggedBuild.buildNumberInt; ActorChargeEvent.isPrimaryEvent and isOneTimeEvent;
ActorCollectionListItem.title and stats; and the TieredPricingPerDatasetItem and
TieredPricingPerEvent types.
`Build.status` was typed as `ACT_JOB_TERMINAL_STATUSES[number]`, so the four terminal statuses
only. That contradicted the client's own documentation: `waitForFinish()` states the returned
build "will have status READY or RUNNING" when the wait times out, and those two were not in the
type. It now uses the full `ACTOR_JOB_STATUSES` union, checked against the spec's `ActorJobStatus`.

`BuildMeta.origin` moves from a bare `string` to the shared `META_ORIGINS` union, likewise
checked against the spec's `RunOrigin`.

`BuildCollectionClientListItem` was assembled out of `Pick<Build, ...>` guesses. It now comes
from `BuildShort`, the schema the list endpoint actually returns, which is where `buildNumber`
and `buildNumberInt` come from.

`OpenApiDefinition` stays hand-written. `GET /v2/actors/{actorId}/builds/{buildId}/openapi.json`
is typed as a bare `object` in the spec, so there is nothing to adapt it to.

BREAKING CHANGE: Build, BuildMeta, BuildStats, BuildUsage, BuildOptions and
BuildCollectionClientListItem now follow the spec's nullability and optionality.
Build.status widens to all eight Actor job statuses; Build.finishedAt, stats, options, usage,
usageUsd, usageTotalUsd, inputSchema, readme and actorDefinition gain `| null`;
BuildMeta.clientIp and userAgent become optional while origin narrows from `string` to the
META_ORIGINS union; every field of BuildStats becomes optional; BuildUsage.ACTOR_COMPUTE_UNITS
and every field of BuildOptions gain `| null`; and BuildCollectionClientListItem is now derived
from the spec's BuildShort, so actId and userId become optional, meta stays optional and
usageTotalUsd and buildNumber become required. Newly exposed: Build.actVersion,
BuildStats.imageSizeBytes and BuildCollectionClientListItem.buildNumberInt.
`ActorRun` no longer extends `ActorRunListItem`. The spec describes the run and the list item as
two separate schemas that genuinely disagree -- `usageTotalUsd` is required on the list item and
optional-nullable on the full resource -- so the inheritance could only be kept by narrowing one
of them to fit the other. Both are now derived independently.

`ActorRun.generalAccess` is the one deliberate narrowing. The spec reuses the storage-wide
`GeneralAccess` schema, which also lists `ANYONE_WITH_NAME_CAN_READ`; a run has no name to be
addressed by, which is exactly why `@apify/consts` declares a separate three-member
`RUN_GENERAL_ACCESS`. That stays the published type, and `spec_guards.ts` asserts subset rather
than equality for it.

`ActorRunOptions.restartOnError` is the one field the spec omits, so it is the whole of
ActorRunOptionsSpecGaps. `ActorRunMeta.origin` moves from a bare `string` to the shared
`META_ORIGINS` union.

`ActorRunUsage` is deliberately still one type. The spec splits the same twelve fields into
`RunUsage` and `RunUsageUsd`, which are structurally identical, so `usage` and `usageUsd` both
keep pointing at the single published name.

BREAKING CHANGE: ActorRun, ActorRunListItem, ActorRunMeta, ActorRunStats, ActorRunOptions,
ActorRunUsage and ActorRunStorageIds now follow the spec's nullability and optionality, and
ActorRun no longer extends ActorRunListItem. ActorRun.containerUrl becomes optional;
finishedAt, statusMessage, exitCode, buildNumber, gitBranchName, usage, usageUsd and
usageTotalUsd gain `| null`; ActorRunListItem.finishedAt becomes optional and nullable while
usageTotalUsd becomes required and userId becomes optional; ActorRunMeta.userAgent becomes
optional and gains `| null`, clientIp gains `| null`, and origin narrows from `string` to the
META_ORIGINS union; every field of ActorRunStats becomes optional and inputBodyLen gains
`| null`; ActorRunOptions.maxItems and maxTotalChargeUsd gain `| null`; every field of
ActorRunUsage gains `| null`; and ActorRunStorageIds no longer guarantees a `default` alias in
any of its three groups, nor the groups themselves. Newly exposed:
ActorRun.isStatusMessageTerminal, metamorphs and platformUsageBillingModel;
ActorRunListItem.buildNumberInt; ActorRunMeta.scheduleId and scheduledAt;
ActorRunStats.migrationCount and rebootCount; and the ActorRunMetamorph type.
`Task.description` is the one field the spec omits, and `TaskList` inherits the same gap, so
`spec_guards.ts` asserts it against both `Task` and `TaskShort`.

`Task.input` keeps the array form the spec drops. The spec models the input as a plain JSON
object, but this same type backs `TaskUpdateData`, so narrowing it would start rejecting
`update({ input: [...] })` calls that work today.

`TaskList` was `Omit<Task, 'options' | 'input'>`, a guess at what the list endpoint returns. It
now comes from `TaskShort`, which is where `actName` and `actUsername` come from.

`PricingInfo` was a one-field `{ pricingModel: string }` placeholder. The spec describes the
whole `CurrentPricingInfo` shape, so the Store listing now carries the trial, unit and
per-event price fields as well. It stays a flat summary rather than one of the
`ActorRunPricingInfo` variants -- that is how the Store endpoint reports it.

BREAKING CHANGE: Task, TaskStats, TaskOptions, TaskList, ActorStoreList and PricingInfo now
follow the spec's nullability and optionality. Task.stats becomes optional and nullable;
Task.username, title, options, input and actorStandby gain `| null`; Task.actorStandby is the
full ActorStandby rather than `Partial<ActorStandby>`; TaskStats.totalRuns becomes optional;
every field of TaskOptions gains `| null`; TaskList is now derived from the spec's TaskShort, so
it no longer carries `title` as an inherited required field and drops nothing else;
ActorStoreList.title becomes required while url and currentPricingInfo become optional, and
description, pictureUrl and userPictureUrl gain `| null`. Newly exposed: Task.removedAt and
standbyUrl; TaskOptions.maxItems and maxTotalChargeUsd; TaskList.actName and actUsername;
ActorStoreList.userFullName, categories, notice, isWhiteListedForAgenticPayments,
actorReviewCount, actorReviewRating, bookmarkCount and badge; and the full set of
PricingInfo fields.
`Webhook.lastDispatch` was typed as `string`. The API returns an object -- the spec's
`ExampleWebhookDispatch`, published here as `WebhookLastDispatch` -- carrying the dispatch status
and its finish and removal times. The old type could not have been right for any caller.

`WebhookCondition` keeps its union of single-id variants. The spec models it as one flat object
with all three ids optional and nullable, but this type also backs `WebhookUpdateData`, where the
flat shape would let a caller send none of the three or all three at once.

`isApifyIntegration` is the one field the spec omits from the full `Webhook` schema while
carrying it on `WebhookShort`, the listing shape, so it is the whole of WebhookSpecGaps.

This also settles the TODO that the Dataset and WebhookDispatch commit left in
`spec_guards.ts`. With `Webhook` spec-derived, `requestUrl` can be pinned against
`WebhookDispatchWebhookSummary` again -- nullable on both sides, because a Slack or email hook
action has no URL. `isAdHoc` is deliberately left unpinned: the spec types it nullable on the
webhook and non-nullable on the summary, and asserting that would only encode the inconsistency.

`WebhookEventType` now lives in `src/models.ts` and is re-exported from
`src/resource_clients/webhook`. The duplicate declaration that existed to avoid an import cycle
is gone, along with the guard that kept the two copies in step.

BREAKING CHANGE: Webhook, WebhookStats and the three WebhookCondition variants now follow the
spec's nullability and optionality. Webhook.lastDispatch changes from `string` to
`WebhookLastDispatch | null` and becomes optional; Webhook.isAdHoc, doNotRetry,
shouldInterpolateStrings, payloadTemplate and requestUrl become optional and nullable;
Webhook.stats becomes optional and nullable; Webhook.headersTemplate and description gain
`| null`; and WebhookStats.totalDispatches becomes optional. Newly added: the
WebhookLastDispatch type.
`Schedule.nextRunAt` and `lastRunAt` were typed as `string`. Both end in `At`, so
`parseDateFields()` has always converted them to `Date` before the caller sees them -- the
published type contradicted the client's own runtime behaviour. They are now `Date | null`.

The action union transfers wholesale. The spec models it as a `oneOf` discriminated on `type`
with a `const` per variant, so only the `type` field itself is overridden, to keep publishing the
`ScheduleActions` runtime enum rather than a bare string literal. `spec_guards.ts` pins both
constants; unlike the other two published enums there is no `@apify/consts` equivalent to check
against as well.

`Schedule.timezone` keeps the curated IANA union from `src/timezones.ts`. The spec types it as a
bare `string`, and this type also backs `ScheduleCreateOrUpdateData`, so widening it would drop
the completion and typo-checking the union exists for.

`ScheduledActorRunOptions` is now the spec's `TaskOptions`, which is the schema the API really
uses for a scheduled run's options; that is where `maxItems` and `maxTotalChargeUsd` come from.

BREAKING CHANGE: Schedule, ScheduleAction and its two variants, ScheduledActorRunInput and
ScheduledActorRunOptions now follow the spec's nullability and optionality.
Schedule.nextRunAt and lastRunAt change from `string` to `Date | null` and become optional;
Schedule.title and description gain `| null`; Schedule.notifications becomes optional and its
`email` becomes optional; ScheduleActionRunActor.runInput and runOptions gain `| null`;
ScheduleActionRunActorTask.input changes from `string` to `object | null`;
ScheduledActorRunInput.body and contentType become optional and nullable; and
ScheduledActorRunOptions.build, timeoutSecs and memoryMbytes become optional and nullable.
Newly exposed: ScheduledActorRunOptions.maxItems and maxTotalChargeUsd.
`UserPlan.enabledPlatformFeatures` is no longer `PlatformFeature[]`. The spec types it as a plain
`string[]`, and the enum was demonstrably incomplete: `PROXY_RESIDENTIAL`, `ACTORS_PUBLIC_ALL` and
`ACTORS_PUBLIC_DEVELOPER` all appear as keys of `EffectivePlatformFeatures` but never made it into
the enum, so using it as the element type promised a completeness that was not real. The enum
stays published, for comparisons.

`MonthlyUsage.dailyServiceUsages[].date` changes from `Date` to `string`. `parseDateFields()` only
converts keys ending in `At`, so the value was never a `Date` at runtime.

`User` is built on `UserPrivateInfo`, with `plan`, `effectivePlatformFeatures` and `isPaying`
widened back to optional. All three are required in that schema, but the same published type also
describes `GET /v2/users/{userId}`, whose `UserPublicInfo` response carries none of them.

`EffectivePlatformFeature`, `EffectivePlatformFeatures` and the user profile shape were private
interfaces. They are now published as named types -- the profile as `UserProfile` -- so the
reference can link to them instead of inlining.

BREAKING CHANGE: User, UserProxy, ProxyGroup, UserPlan, MonthlyUsage, UsageCycle,
AccountAndUsageLimits, MonthlyUsageCycle, Limits and Current now follow the spec's nullability
and optionality. User.profile becomes optional; UserPlan.enabledPlatformFeatures changes from
PlatformFeature[] to string[] and every other field of UserPlan becomes optional;
ProxyGroup.description gains `| null`; UserProfile.bio, pictureUrl, githubUsername, websiteUrl
and twitterUsername gain `| null`; and the daily usage entries carry `date` as a string. Newly
exposed: UserPlan.tier, apiRateLimitBoosts, maxScheduleCount, maxConcurrentActorRuns and
planPricing; Limits.maxScheduleCount; Current.scheduleCount; and the UserProfile,
EffectivePlatformFeature and EffectivePlatformFeatures types.
Three published types were describing the wrong shape.
`RequestQueueClientGetRequestResult` was `Omit<RequestQueueClientListItem, 'retryCount'>`, a
queue-head projection, while `GET .../requests/{requestId}` answers with the whole request.
`RequestQueueClientRequestSchema.handledAt` was `string` although it ends in `At`, so
`parseDateFields()` has always handed the caller a `Date`. And `RequestQueueClientListItem`
conflated the plain and locked head requests, which the spec separates -- the locked one is now
published as `RequestQueueClientLockedListItem`, and `lockExpiresAt` is required on it rather
than optional on both.

`RequestQueueClientListAndLockHeadResult` no longer extends `RequestQueueClientListHeadResult`.
The spec describes the two as separate schemas that disagree about which fields are required, and
the locked variant carries a different element type.

Batch and add methods now take `RequestQueueClientRequestToAdd` rather than
`Omit<RequestQueueClientRequestSchema, 'id'>`. Once `uniqueKey` and `url` became optional on the
stored request -- which is correct for a response -- the old input type allowed submissions the
API rejects, and `batchAddRequests()` could no longer build a valid retry list from them.

`title` is absent from every queue schema in the spec; `username` and `expireAt` are carried on
`RequestQueueShort`, the listing shape, and omitted from the full `RequestQueue`. All three are
tracked as spec gaps. `consoleUrl` is kept optional for the same reason as on Dataset: the client
types `requestQueues().list()` items as this model, and the listing does not carry it.

Known follow-up, unchanged by this commit: `RequestQueueClientBatchRequestsOperationResult` is
derived from the spec's `BatchAddResult` and is still used as the return type of the batch delete
path too, where the API answers with `BatchDeleteResult` and reports processed requests by id or
unique key rather than as added requests.

BREAKING CHANGE: RequestQueue, RequestQueueStats, RequestQueueClientListItem,
RequestQueueClientListHeadResult, RequestQueueClientListAndLockHeadResult,
RequestQueueClientListRequestsResult, RequestQueueClientRequestSchema,
RequestQueueClientGetRequestResult, RequestQueueClientAddRequestResult,
RequestQueueClientProlongRequestLockResult, RequestQueueClientUnlockRequestsResult,
RequestQueueClientBatchRequestsOperationResult and RequestQueueClientRequestToDelete now follow
the spec. RequestQueue.name, actId and actRunId gain `| null` and stats becomes optional;
RequestQueueClientListItem.retryCount becomes optional and loses `lockExpiresAt`;
RequestQueueClientRequestSchema.id, uniqueKey and url become optional, handledAt changes from
`string` to `Date | null`, payload widens to `string | object | null`, headers to `object | null`,
and errorMessages, noRetry and loadedUrl gain `| null`; RequestQueueClientGetRequestResult is now
the full request schema; RequestQueueClientListAndLockHeadResult.clientKey and
queueHasLockedRequests become optional; and the four batch and add methods require `uniqueKey`
and `url` on every submitted request. Newly exposed: RequestQueue.consoleUrl,
RequestQueueClientLockedListItem and RequestQueueClientRequestToAdd.
…uild stamp

Renovate cannot see the spec -- it is a live document, not an npm dependency -- so without this
workflow the committed snapshot never moves and none of the drift guards in src/spec_guards.ts
can ever fire.

The refresh cannot compare the file byte for byte. Apify's `info.version` is an apify-docs *build*
stamp (`v2-2026-07-28T083939Z`), not an API version, so it changes on every docs deploy whether
or not a single endpoint moved; a byte comparison would open a pull request nearly every week
whose whole diff is that one line. `isOnlyBuildStampChange()` compares the two revisions with
that field dropped, and a stamp-only refresh is discarded without regenerating anything.

An unparseable revision counts as a real change on purpose. If docs.apify.com answers with an
error page, that should surface as a pull request to look at rather than be silently thrown away.

Deliberately not automerged. A spec change can widen nullability across hundreds of fields or
fill a gap that makes a `*SpecGaps` entry stale, and both need a human to read the diff.
@vdusek vdusek added adhoc Ad-hoc unplanned task added during the sprint. t-tooling Issues with this label are in the ownership of the tooling team. labels Jul 30, 2026
@vdusek vdusek self-assigned this Jul 30, 2026
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

⚠️ There are broken links in the documentation.

See more at https://github.com/apify/apify-client-js/actions/runs/30617512394#summary-91114025972

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

adhoc Ad-hoc unplanned task added during the sprint. t-tooling Issues with this label are in the ownership of the tooling team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants