From db9a7a7aa6fc4e3680e5125f480f87bd0cc6b610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=28Honza=29=20Jir=C3=A1=C5=88?= Date: Mon, 3 Aug 2026 08:14:02 +0200 Subject: [PATCH 1/4] feat: add Actor task publication endpoints --- src/resource_clients/task.ts | 60 ++++++++++++++++++++++++++++++-- test/mock_server/routes/tasks.ts | 2 ++ test/tasks.test.ts | 24 +++++++++++++ 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/src/resource_clients/task.ts b/src/resource_clients/task.ts index e7e22f7d9..16edf9ce9 100644 --- a/src/resource_clients/task.ts +++ b/src/resource_clients/task.ts @@ -67,6 +67,44 @@ export class TaskClient extends ResourceClient { return this._update(newFields); } + /** + * Publishes the task on its public landing page. + * + * The task's Actor must be public and the task must have its public display configuration + * (`publicConfig`) set up via {@apilink TaskClient.update}. + * + * @returns The task object. + * @see https://docs.apify.com/api/v2/actor-task-publish-post + */ + async publish(): Promise { + const response = await this.httpClient.call({ + url: this._url('publish'), + method: 'POST', + params: this._params(), + }); + + return cast(parseDateFields(pluckData(response.data))); + } + + /** + * Unpublishes the task from its public landing page. + * + * The public display configuration (`publicConfig`) is preserved, so the task can be + * published again without re-entering it. + * + * @returns The task object. + * @see https://docs.apify.com/api/v2/actor-task-unpublish-post + */ + async unpublish(): Promise { + const response = await this.httpClient.call({ + url: this._url('unpublish'), + method: 'POST', + params: this._params(), + }); + + return cast(parseDateFields(pluckData(response.data))); + } + /** * Deletes the Task. * @@ -293,6 +331,24 @@ export interface Task { options?: TaskOptions; input?: Dictionary | Dictionary[]; actorStandby?: Partial; + publicConfig?: TaskPublicConfig | null; +} + +/** + * Public-facing display configuration of a task's public landing page. + * + * The task is published when `publishedAt` is set and unpublished when it is `null`. The + * `publishedAt` field is read-only - use {@apilink TaskClient.publish} and + * {@apilink TaskClient.unpublish} to change the publication state. + */ +export interface TaskPublicConfig { + publishedAt: Date | null; + seoTitle?: string | null; + seoDescription?: string | null; + categorization?: string | null; + inputSchemaFields?: string[] | null; + datasetName?: string | null; + datasetView?: string | null; } /** @@ -317,12 +373,12 @@ export interface TaskOptions { */ export type TaskUpdateData = Partial< Pick ->; +> & { publicConfig?: Omit | null }; /** * Options for filtering the last run of a Task. */ -export interface TaskLastRunOptions extends ActorLastRunOptions {} +export interface TaskLastRunOptions extends ActorLastRunOptions { } /** * Options for starting a Task. diff --git a/test/mock_server/routes/tasks.ts b/test/mock_server/routes/tasks.ts index 04ceee14f..f0f17fbb4 100644 --- a/test/mock_server/routes/tasks.ts +++ b/test/mock_server/routes/tasks.ts @@ -10,6 +10,8 @@ const ROUTES: MockServerRoute[] = [ { id: 'update-task', method: 'PUT', path: '/:taskId' }, { id: 'delete-task', method: 'DELETE', path: '/:taskId' }, { id: 'get-task', method: 'GET', path: '/:taskId' }, + { id: 'publish-task', method: 'POST', path: '/:taskId/publish' }, + { id: 'unpublish-task', method: 'POST', path: '/:taskId/unpublish' }, { id: 'list-runs', method: 'GET', path: '/:taskId/runs' }, { id: 'run-task', method: 'POST', path: '/:taskId/runs', type: 'responseJsonMock' }, { id: 'list-webhooks', method: 'GET', path: '/:taskId/webhooks' }, diff --git a/test/tasks.test.ts b/test/tasks.test.ts index e265161f9..0bcf33968 100644 --- a/test/tasks.test.ts +++ b/test/tasks.test.ts @@ -97,6 +97,30 @@ describe('Task methods', () => { validateRequest({ query: {}, params: { taskId } }); }); + test('publish() works', async () => { + const taskId = 'some-task-id'; + + const res = await client.task(taskId).publish(); + expect(res.id).toEqual('publish-task'); + validateRequest({ query: {}, params: { taskId } }); + + const browserRes = await page.evaluate((id) => client.task(id).publish(), taskId); + expect(browserRes).toEqual(res); + validateRequest({ query: {}, params: { taskId } }); + }); + + test('unpublish() works', async () => { + const taskId = 'some-task-id'; + + const res = await client.task(taskId).unpublish(); + expect(res.id).toEqual('unpublish-task'); + validateRequest({ query: {}, params: { taskId } }); + + const browserRes = await page.evaluate((id) => client.task(id).unpublish(), taskId); + expect(browserRes).toEqual(res); + validateRequest({ query: {}, params: { taskId } }); + }); + test('get() works', async () => { const taskId = 'some-id'; From 8013ff53672e119a144eb12aac44b659a0f44f77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=28Honza=29=20Jir=C3=A1=C5=88?= Date: Mon, 3 Aug 2026 08:20:07 +0200 Subject: [PATCH 2/4] chore: lint --- src/resource_clients/task.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/resource_clients/task.ts b/src/resource_clients/task.ts index 16edf9ce9..fb9ba8727 100644 --- a/src/resource_clients/task.ts +++ b/src/resource_clients/task.ts @@ -378,7 +378,7 @@ export type TaskUpdateData = Partial< /** * Options for filtering the last run of a Task. */ -export interface TaskLastRunOptions extends ActorLastRunOptions { } +export interface TaskLastRunOptions extends ActorLastRunOptions {} /** * Options for starting a Task. From 97fdb83b3edf7e7542791fd6518a6a335bf7a997 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=28Honza=29=20Jir=C3=A1=C5=88?= Date: Mon, 3 Aug 2026 14:14:02 +0200 Subject: [PATCH 3/4] feat: review feedback --- src/resource_clients/task.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/resource_clients/task.ts b/src/resource_clients/task.ts index fb9ba8727..8a044f557 100644 --- a/src/resource_clients/task.ts +++ b/src/resource_clients/task.ts @@ -71,7 +71,11 @@ export class TaskClient extends ResourceClient { * Publishes the task on its public landing page. * * The task's Actor must be public and the task must have its public display configuration - * (`publicConfig`) set up via {@apilink TaskClient.update}. + * (`publicConfig`) set up via {@apilink TaskClient.update}. Requires write permission to + * both the task and its Actor. + * + * Publishing an already published task is a no-op that returns the current state, so this + * call is safe to retry (unlike {@apilink TaskClient.unpublish}). * * @returns The task object. * @see https://docs.apify.com/api/v2/actor-task-publish-post @@ -90,7 +94,11 @@ export class TaskClient extends ResourceClient { * Unpublishes the task from its public landing page. * * The public display configuration (`publicConfig`) is preserved, so the task can be - * published again without re-entering it. + * published again without re-entering it. Requires write permission to both the task + * and its Actor. + * + * Unlike {@apilink TaskClient.publish}, this call is not idempotent: unpublishing a task + * that is not currently published throws an `ApifyApiError` (`cannot-unpublish-actor-task`). * * @returns The task object. * @see https://docs.apify.com/api/v2/actor-task-unpublish-post @@ -373,7 +381,7 @@ export interface TaskOptions { */ export type TaskUpdateData = Partial< Pick -> & { publicConfig?: Omit | null }; +> & { publicConfig?: Omit }; /** * Options for filtering the last run of a Task. From 839d78ef7f70d19bbe233ac21330a6612f53780f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=28Honza=29=20Jir=C3=A1=C5=88?= Date: Thu, 6 Aug 2026 10:18:49 +0200 Subject: [PATCH 4/4] feat: remove publish endpoint and move the logic to PUT request [internal] --- src/resource_clients/task.ts | 47 ++++++++++++-------------------- test/mock_server/routes/tasks.ts | 2 -- test/tasks.test.ts | 12 ++++---- 3 files changed, 24 insertions(+), 37 deletions(-) diff --git a/src/resource_clients/task.ts b/src/resource_clients/task.ts index 8a044f557..166983cac 100644 --- a/src/resource_clients/task.ts +++ b/src/resource_clients/task.ts @@ -68,49 +68,33 @@ export class TaskClient extends ResourceClient { } /** - * Publishes the task on its public landing page. + * Publishes the task on its public landing page, by setting `isPublic` through + * {@apilink TaskClient.update}. * * The task's Actor must be public and the task must have its public display configuration - * (`publicConfig`) set up via {@apilink TaskClient.update}. Requires write permission to - * both the task and its Actor. - * - * Publishing an already published task is a no-op that returns the current state, so this - * call is safe to retry (unlike {@apilink TaskClient.unpublish}). + * (`publicConfig`) set up first. Requires write permission to both the task and its Actor. + * Publishing an already published task does nothing. * * @returns The task object. - * @see https://docs.apify.com/api/v2/actor-task-publish-post + * @see https://docs.apify.com/api/v2/actor-task-put */ async publish(): Promise { - const response = await this.httpClient.call({ - url: this._url('publish'), - method: 'POST', - params: this._params(), - }); - - return cast(parseDateFields(pluckData(response.data))); + return this.update({ isPublic: true }); } /** - * Unpublishes the task from its public landing page. + * Unpublishes the task from its public landing page, by setting `isPublic` through + * {@apilink TaskClient.update}. * * The public display configuration (`publicConfig`) is preserved, so the task can be - * published again without re-entering it. Requires write permission to both the task - * and its Actor. - * - * Unlike {@apilink TaskClient.publish}, this call is not idempotent: unpublishing a task - * that is not currently published throws an `ApifyApiError` (`cannot-unpublish-actor-task`). + * published again without re-entering it. Requires write permission to both the task and its + * Actor. Unpublishing a task that is not published does nothing. * * @returns The task object. - * @see https://docs.apify.com/api/v2/actor-task-unpublish-post + * @see https://docs.apify.com/api/v2/actor-task-put */ async unpublish(): Promise { - const response = await this.httpClient.call({ - url: this._url('unpublish'), - method: 'POST', - params: this._params(), - }); - - return cast(parseDateFields(pluckData(response.data))); + return this.update({ isPublic: false }); } /** @@ -339,6 +323,11 @@ export interface Task { options?: TaskOptions; input?: Dictionary | Dictionary[]; actorStandby?: Partial; + /** + * Whether the task is published on its public landing page. Derived from + * `publicConfig.publishedAt` — set it on update to publish or unpublish the task. + */ + isPublic?: boolean; publicConfig?: TaskPublicConfig | null; } @@ -380,7 +369,7 @@ export interface TaskOptions { * Fields that can be updated when modifying a Task. */ export type TaskUpdateData = Partial< - Pick + Pick > & { publicConfig?: Omit }; /** diff --git a/test/mock_server/routes/tasks.ts b/test/mock_server/routes/tasks.ts index f0f17fbb4..04ceee14f 100644 --- a/test/mock_server/routes/tasks.ts +++ b/test/mock_server/routes/tasks.ts @@ -10,8 +10,6 @@ const ROUTES: MockServerRoute[] = [ { id: 'update-task', method: 'PUT', path: '/:taskId' }, { id: 'delete-task', method: 'DELETE', path: '/:taskId' }, { id: 'get-task', method: 'GET', path: '/:taskId' }, - { id: 'publish-task', method: 'POST', path: '/:taskId/publish' }, - { id: 'unpublish-task', method: 'POST', path: '/:taskId/unpublish' }, { id: 'list-runs', method: 'GET', path: '/:taskId/runs' }, { id: 'run-task', method: 'POST', path: '/:taskId/runs', type: 'responseJsonMock' }, { id: 'list-webhooks', method: 'GET', path: '/:taskId/webhooks' }, diff --git a/test/tasks.test.ts b/test/tasks.test.ts index 0bcf33968..3489e9186 100644 --- a/test/tasks.test.ts +++ b/test/tasks.test.ts @@ -99,26 +99,26 @@ describe('Task methods', () => { test('publish() works', async () => { const taskId = 'some-task-id'; + const body = { isPublic: true }; const res = await client.task(taskId).publish(); - expect(res.id).toEqual('publish-task'); - validateRequest({ query: {}, params: { taskId } }); + validateRequest({ query: {}, params: { taskId }, body, endpointId: 'update-task' }); const browserRes = await page.evaluate((id) => client.task(id).publish(), taskId); expect(browserRes).toEqual(res); - validateRequest({ query: {}, params: { taskId } }); + validateRequest({ query: {}, params: { taskId }, body }); }); test('unpublish() works', async () => { const taskId = 'some-task-id'; + const body = { isPublic: false }; const res = await client.task(taskId).unpublish(); - expect(res.id).toEqual('unpublish-task'); - validateRequest({ query: {}, params: { taskId } }); + validateRequest({ query: {}, params: { taskId }, body, endpointId: 'update-task' }); const browserRes = await page.evaluate((id) => client.task(id).unpublish(), taskId); expect(browserRes).toEqual(res); - validateRequest({ query: {}, params: { taskId } }); + validateRequest({ query: {}, params: { taskId }, body }); }); test('get() works', async () => {