Prevent Unpublishing of Singleton Models

Hey @felixh, the short answer is that plugins cannot block scheduled unpublishing. onBeforeItemsUnpublish only fires for immediate unpublish actions triggered in the UI, while schedules are created in the UI but executed later by the backend, outside of the plugin runtime. You can see the available hooks here: https://www.datocms.com/docs/plugin-sdk/event-hooks.

If you need to enforce “never unpublish singletons” across both immediate and scheduled actions, the usual workaround is to cancel schedules as soon as they are created. When someone sets an unpublishing schedule, DatoCMS sends an update webhook and the record payload includes meta.unpublishing_scheduled_at. In your webhook handler you can check if the record’s model is a singleton and immediately call the Scheduled Unpublishing destroy endpoint for that item. Docs for scheduled unpublishing are here: create https://www.datocms.com/docs/content-management-api/resources/scheduled-unpublishing/create and delete https://www.datocms.com/docs/content-management-api/resources/scheduled-unpublishing/destroy. The item payload exposes meta.unpublishing_scheduled_at so it is easy to detect, see the record object here: https://www.datocms.com/docs/content-management-api/resources/item. Webhooks overview is here: https://www.datocms.com/docs/general-concepts/webhooks.

Here is a minimal example using our official client to auto‑cancel any scheduled unpublish on singleton models:

import express from 'express';
import { buildClient } from '@datocms/cma-client-node';

const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

const app = express();
app.use(express.json());

app.post('/dato-webhook', async (req, res) => {
  try {
    const { event_type, entity_type, entity } = req.body;

    if (entity_type === 'item' && event_type === 'update') {
      const scheduledAt = entity?.meta?.unpublishing_scheduled_at;
      if (scheduledAt) {
        const itemTypeId = entity?.relationships?.item_type?.data?.id;
        const itemType = await client.itemTypes.find(itemTypeId); // https://www.datocms.com/docs/content-management-api/resources/item-type
        if (itemType.singleton) {
          await client.scheduledUnpublishing.destroy(entity.id); // https://www.datocms.com/docs/content-management-api/resources/scheduled-unpublishing/destroy
        }
      }
    }

    res.status(200).send('ok');
  } catch (e) {
    console.error(e);
    res.status(500).send('error');
  }
});

app.listen(3000);

If you prefer to prevent schedules at the source, remember scheduling uses the same “Publish/unpublish” permission used for immediate actions. You can restrict who is allowed to unpublish at the role level or via API tokens, but note that publish and unpublish are a single permission today. Roles and permissions are documented at https://www.datocms.com/docs/general-concepts/roles-and-permission-system, and there is an open feature request to split publish from unpublish here: https://community.datocms.com/t/separate-publish-unpublish-permission/5323.

So, combine the UI plugin you have for immediate actions with a small webhook like the one above to neutralize scheduled unpublishing as soon as it is created. That covers both paths.