Work with apps in Release context

This guide explains how to ensure your apps work correctly when entries are being edited within a release context, and how to implement opt-out mechanisms when your app is not compatible with releases.

Overview

Contentful’s Timeline feature allows content editors to manage, preview, and schedule changes to a batch of entries. When an entry is being edited within a release context, some apps may encounter compatibility issues or need to adjust their behavior.

This guide provides practical solutions for handling release contexts in your apps, including opt-out mechanisms for apps that cannot function properly within releases.

NOTE: Apps in entry-related locations (Entry Field, Entry Sidebar, Entry Editor) are always affected by releases. Dialog apps inherit the release context when opened from entry-related locations but are unaffected when opened from other locations. Apps in Page, Home, and App Configuration locations are never affected by release contexts.

Understanding Release Context

When a content editor works with entries in a release, they’re editing a release version of the entry that exists separately from the main entry. This has several implications for apps:

  • All entry data and references (including sdk.entry) are scoped to the release context, not the base entries
  • Some SDK methods like publish() and unpublish() are not available because the release itself manages the publishing state of all included entries
  • The sdk.release attribute contains information about the current release

Detecting Release Context

The primary way to detect whether your app is running in a release context is by checking the sdk.release attribute:

1import { init } from '@contentful/app-sdk';
2
3init((sdk) => {
4 if (sdk.release) {
5 // App is running in release context
6 console.log('Release title:', sdk.release.title);
7 } else {
8 // App is running in normal (base entry) context
9 console.log('App is running in base entry context');
10 }
11});

Release Object Structure

When in release context, sdk.release contains the following information:

1{
2 "sys": {
3 "type": "Release",
4 "id": "release-id",
5 "schemaVersion": "Release.v2",
6 "version": 1,
7 "space": {
8 "sys": { "type": "Link", "linkType": "Space", "id": "space-id" }
9 },
10 "environment": {
11 "sys": { "type": "Link", "linkType": "Environment", "id": "master" }
12 },
13 "createdAt": "2024-11-29T12:00:00.000Z",
14 "updatedAt": "2024-11-29T12:00:00.000Z",
15 "createdBy": { "sys": { "type": "Link", "linkType": "User", "id": "user-id" } },
16 "updatedBy": { "sys": { "type": "Link", "linkType": "User", "id": "user-id" } }
17 },
18 "title": "Black Friday Campaign",
19 "description": "Campaign for Black Friday promotion",
20 "entities": {
21 "sys": { "type": "Array" },
22 "items": [
23 {
24 "entity": {
25 "sys": {
26 "type": "Link",
27 "linkType": "Entry", // or "Asset"
28 "id": "entry-id"
29 }
30 },
31 "action": "publish", // or "unpublish"
32 }
33 ]
34 }
35}

When not in release context, sdk.release will be undefined.

Opt-out patterns

There are several approaches you can take when your app encounters a release context:

Complete opt-out

The simplest approach is to render nothing when in release context:

1import React from 'react';
2import { useSDK } from '@contentful/react-apps-toolkit';
3
4const Sidebar = () => {
5 const sdk = useSDK();
6
7 // Complete opt-out: render nothing in release context
8 if (sdk.release) {
9 return null;
10 }
11
12 // Normal app functionality for base entries
13 return (
14 <div>
15 <h3>My App</h3>
16 <p>This app works with base entries only.</p>
17 </div>
18 );
19};
20
21export default Sidebar;

Partial opt-out with fallback UI

Show a user-friendly message explaining why the app is not available:

1import React from 'react';
2import { Paragraph, Note } from '@contentful/f36-components';
3import { useSDK } from '@contentful/react-apps-toolkit';
4
5const FieldEditor = () => {
6 const sdk = useSDK();
7
8 // Partial opt-out: show explanatory message
9 if (sdk.release) {
10 return (
11 <Info variant="warning" title="Not available in releases">
12 <Paragraph>
13 This app is not compatible with release entries. Please edit this
14 field in the base entry or contact your administrator.
15 </Paragraph>
16 </Info>
17 );
18 }
19
20 // Normal field editor functionality
21 return <div>{/* Your field editor implementation */}</div>;
22};
23
24export default FieldEditor;

Conditional feature disabling

Disable specific features while keeping the app partially functional:

1import React, { useState } from 'react';
2import { Button, Stack, Paragraph } from '@contentful/f36-components';
3import { useSDK } from '@contentful/react-apps-toolkit';
4
5const EntryEditor = () => {
6 const sdk = useSDK();
7 const [isPublishing, setIsPublishing] = useState(false);
8
9 const isInReleaseContext = !!sdk.release;
10
11 const handlePublish = async () => {
12 if (isInReleaseContext) {
13 sdk.notifier.error('Publishing is not available in release context');
14 return;
15 }
16
17 setIsPublishing(true);
18 try {
19 await sdk.entry.publish();
20 sdk.notifier.success('Entry published successfully');
21 } catch (error) {
22 sdk.notifier.error('Failed to publish entry');
23 } finally {
24 setIsPublishing(false);
25 }
26 };
27
28 return (
29 <Stack flexDirection="column" spacing="spacingM">
30 {isInReleaseContext && (
31 <Paragraph>
32 You are editing this entry in release context: "{sdk.release.title}"
33 </Paragraph>
34 )}
35
36 <Button
37 variant="primary"
38 isDisabled={isInReleaseContext || isPublishing}
39 isLoading={isPublishing}
40 onClick={handlePublish}
41 >
42 {isInReleaseContext
43 ? 'Publish (Not available in releases)'
44 : 'Publish Entry'}
45 </Button>
46
47 {/* Other app functionality that works in both contexts */}
48 </Stack>
49 );
50};
51
52export default EntryEditor;

Affected App Locations

Apps in the following locations are affected by release contexts:

LocationConstantImpact
Entry FieldLOCATION_ENTRY_FIELDField values are release-scoped
Entry SidebarLOCATION_ENTRY_SIDEBAREntry data is release-scoped
Entry EditorLOCATION_ENTRY_EDITOREntire entry is release-scoped
DialogLOCATION_DIALOGInherits release context when opened from entry-related locations above

Apps in these locations are not affected by releases:

  • Page (LOCATION_PAGE)
  • Home (LOCATION_HOME)
  • App Configuration (LOCATION_APP_CONFIG)

Dialog Context Inheritance

When a dialog is opened from a release-aware location (Entry Field, Entry Sidebar, or Entry Editor), it inherits the parent’s release context. This means:

  • sdk.release will be defined in the dialog app
  • Entity selector dialogs (selectSingleEntry, selectMultipleEntries, etc.) will show release-scoped entities
  • The dialog app should implement the same release-aware patterns as other entry-related apps

However, if the same dialog is opened from a non-release location (Page, Home, App Configuration), it will not have release context.

Unsupported Features in Release Context

The following SDK methods and features are not available when working with release-scoped entries:

Entry methods

  • sdk.entry.publish() - Throws error: “Publish entry method is not supported in release context”
  • sdk.entry.unpublish() - Throws error: “Unpublish entry method is not supported in release context”

Code examples

Entry field app with release awareness

1import React, { useState, useEffect } from 'react';
2import { TextInput, FormControl, HelpText } from '@contentful/f36-components';
3import { useSDK } from '@contentful/react-apps-toolkit';
4
5const CustomFieldEditor = () => {
6 const sdk = useSDK();
7 const [value, setValue] = useState(sdk.field.getValue() || '');
8
9 // Handle release context
10 if (sdk.release) {
11 return (
12 <FormControl>
13 <FormControl.Label>Custom Field (Release Mode)</FormControl.Label>
14 <TextInput
15 value={value}
16 onChange={(e) => {
17 setValue(e.target.value);
18 sdk.field.setValue(e.target.value);
19 }}
20 />
21 <HelpText>Editing in release: {sdk.release.title}</HelpText>
22 </FormControl>
23 );
24 }
25
26 // Normal field editor
27 return (
28 <FormControl>
29 <FormControl.Label>Custom Field</FormControl.Label>
30 <TextInput
31 value={value}
32 onChange={(e) => {
33 setValue(e.target.value);
34 sdk.field.setValue(e.target.value);
35 }}
36 />
37 </FormControl>
38 );
39};
40
41export default CustomFieldEditor;

Entry sidebar app with conditional features

1import React from 'react';
2import { Card, Paragraph, Button, Stack } from '@contentful/f36-components';
3import { useSDK } from '@contentful/react-apps-toolkit';
4
5const EntrySidebar = () => {
6 const sdk = useSDK();
7
8 const handleExternalSync = () => {
9 if (sdk.release) {
10 sdk.notifier.warning(
11 'External sync is not available for release entries'
12 );
13 return;
14 }
15 // Perform external sync
16 };
17
18 return (
19 <Card>
20 <Stack flexDirection="column" spacing="spacingM">
21 <Paragraph>
22 {sdk.release ? `Release: ${sdk.release.title}` : 'Base Entry'}
23 </Paragraph>
24
25 <Button
26 size="small"
27 isDisabled={!!sdk.release}
28 onClick={handleExternalSync}
29 >
30 Sync to External System
31 </Button>
32
33 {sdk.release && (
34 <Paragraph marginTop="spacingS" fontColor="gray600">
35 Some features are disabled in release context
36 </Paragraph>
37 )}
38 </Stack>
39 </Card>
40 );
41};
42
43export default EntrySidebar;