Ads#
The Storyteller SDK supports displaying ads that can be created in the Storyteller CMS (First Party Ads), as well as Ads from Google Ad Manager via an SDK extension developed by Storyteller and Ads from other sources via custom implementation provided by the integrator.
Which source of ads is used can be configured on your behalf by a member of the Storyteller Delivery Team.
Storyteller First Party Ads#
If your tenant is configured to use Storyteller First Party Ads, which can be managed in the Storyteller CMS, then no changes to the Storyteller integration code are necessary. The Ads code is managed entirely within the Storyteller SDK.
Storyteller GAM Ads#
To use Ads from Google Ad Manager in Storyteller, first reach out to your Storyteller contact and they will assist you with setting up Google Ad Manager to traffic ads to Storyteller.
Required Platform Configuration#
IMPORTANT: Before using the Storyteller GAM Ads package, you must complete the following platform-specific configuration steps:
iOS Configuration (Required)#
Update your Info.plist file following Google's instructions: AdMob iOS Quick Start - Update Info.plist
Android Configuration (Required)#
Update your AndroidManifest.xml file following Google's instructions: AdMob Android Quick Start - Import Mobile Ads SDK
Package Installation and Setup#
Once the platform configuration is complete, you can use the Storyteller GAM Ads package to fetch ads from Google Ad Manager.
On Android, the 11.6.0 React Native release resolves Google Mobile Ads 23.4.0 from the Storyteller native ads artifact and bundles Meta mediation adapter 6.18.0.0, whose minimum supported Google Mobile Ads version is 23.0.0.
To initialize GAM package, call setupGAMModule method:
import StorytellerGamSdk, {
setupGAMModule,
getAdUnitForRequest,
setAdUnitResult,
type AdRequestPayload,
} from '@getstoryteller/react-native-storyteller-sdk-gam';
// Initialize the GAM module with your configuration
setupGAMModule({
customNativeTemplateId: {
stories: 'YOUR_STORIES_TEMPLATE_ID',
clips: 'YOUR_CLIPS_TEMPLATE_ID',
},
bottomBannerAdUnit: '/YOUR_NETWORK_CODE/clips-bottom-banner-ad-unit',
customKvps: {
custom_key: 'custom_value',
},
});
Configuration parameters are explained below:
customNativeTemplateId- If you have worked with the Storyteller Delivery team to setup Custom Native Ads, you will need to supply their IDs here. If you are only using Stories (but not Clips) it is only necessary to supply one property of this structbottomBannerAdUnit- Optional GAM ad unit ID used for Clips bottom banner ads. Provide this when your tenant or collection is configured to show Clips bottom banner ads.customKvps- The Storyteller GAM SDK passes a default set of KVPs to GAM to allow targeting based on the content of the Stories/Clips the user is viewing. If you have any additional parameters that you need to be able to target by, these should be passed here. Note that the SDK will not inherit any KVPs being set in the rest of your app.
setupGAMModule and setupAdMobModule replace only the active Google ads family, so GAM and AdMob remain mutually exclusive while a supported VAST or Android IMA module stays registered. To remove GAM or AdMob without changing other ads modules, call disableGoogleAdsModule():
import { disableGoogleAdsModule } from '@getstoryteller/react-native-storyteller-sdk-gam';
disableGoogleAdsModule();
Disabling or replacing the Google ads module completes any pending JavaScript ad-unit requests with an empty result. It does not remove the consumer-owned getAdUnitForRequest subscription or clear a native ad-request enricher registered by the host app.
Storyteller IMA Pre-roll Ads (Android Only)#
The optional IMA package adds one Google IMA pre-roll before the initially opened Clip on supported Android phone Clips routes. It is independent from standard between-Clips GAM, AdMob, and VAST ads: it is not part of betweenClipsAdProviderOrder and does not provide Stories, banners, mid-rolls, repeated cadence, or Android TV ads.
Install the package at the same version as the core SDK:
yarn add @getstoryteller/react-native-storyteller-sdk-ima
The package is excluded from iOS autolinking. Calling either setup function on iOS throws an Android-only error.
Choose one setup mode during app startup. Storyteller can build the IMA tag from GAM fields:
import { setupImaGamModule } from '@getstoryteller/react-native-storyteller-sdk-ima';
setupImaGamModule({
adUnit: '/YOUR_NETWORK_CODE/clips-preroll',
descriptionUrl: 'https://example.com/clips',
contentUrl: 'https://example.com/clips/content',
customParams: {
section: 'sports',
},
tagParameters: {
npa: '1',
},
});
Alternatively, provide one complete HTTPS IMA/VAST tag:
import { setupImaAdTagModule } from '@getstoryteller/react-native-storyteller-sdk-ima';
setupImaAdTagModule({
adTagUrl: 'https://pubads.g.doubleclick.net/gampad/ads?...',
});
To remove the active IMA integration at runtime, for example when applying an ads configuration
where IMA is disabled, call disableImaModule(). This unregisters only the IMA module and leaves
the configured GAM, AdMob, or VAST module unchanged.
import { disableImaModule } from '@getstoryteller/react-native-storyteller-sdk-ima';
disableImaModule();
Both modes accept an optional synchronous or asynchronous requestConfiguration(adRequest) callback. It receives the collection ID, current Clip metadata, optional next Clip metadata, and ad index. Return a complete request-specific snapshot; callback values override any static fallback values supplied to setup. Resolve promptly: the Android bridge has a one-second response window before it uses the static fallback.
setupImaGamModule({
adUnit: '/YOUR_NETWORK_CODE/default-preroll',
descriptionUrl: 'https://example.com/clips',
requestConfiguration: async ({ collection, clip, nextClip, adIndex }) => ({
adUnit: selectAdUnit(collection, clip.contentId),
descriptionUrl: 'https://example.com/clips',
contentUrl: nextClip?.contentId ? `https://example.com/clips/${nextClip.contentId}` : undefined,
customParams: {
collection,
adIndex: String(adIndex),
},
}),
});
vastLoadTimeoutMs and mediaLoadTimeoutMs are optional positive-integer IMA timeouts. A complete adTagUrl must use HTTPS and be no longer than 8,192 characters. If the JavaScript callback throws, rejects, or misses the native response window, the module falls back to the static setup values; late callback responses are ignored. Avoid logging complete tags or targeting values because they may contain sensitive information.
Android Native Request Enrichment#
Android integrations that need APS, Nimbus, or another bidder SDK to mutate the prepared AdManagerAdRequest.Builder can register a native enricher before JavaScript calls setupGAMModule. Builder and bidder response objects stay in Android code; they are not serialized through React Native.
import com.storytellersdkgam.StorytellerGamAdRequestEnricher
import com.storytellersdkgam.StorytellerSdkGamModule
// Replace this body with your bidder SDK's prepared-request integration.
StorytellerSdkGamModule.registerAdRequestEnricher(
StorytellerGamAdRequestEnricher { requestInfo, builder, completion ->
bidder.load(requestInfo.itemInfo.contentId) { bidderBuilder, error ->
when {
bidderBuilder != null -> completion.onSuccess(bidderBuilder)
error != null -> completion.onFailure()
else -> completion.onNoFill()
}
}
}
)
The callback may call completion.onSuccess(builder) immediately for synchronous enrichment or complete later after host-owned asynchronous work. Call exactly one of onSuccess, onNoFill, or onFailure for each active request. Storyteller reapplies its own targeting when a replacement builder is returned. Call StorytellerSdkGamModule.clearAdRequestEnricher() to remove the hook before a later setupGAMModule call.
iOS Native Request Enrichment#
iOS integrations using Storyteller iOS 11.6.0 or newer can register the corresponding native Swift hook before JavaScript calls setupGAMModule. The callback runs asynchronously on the main actor and receives the prepared Google Request by reference, so Google and bidder objects remain entirely in native code.
import GoogleMobileAds
import StorytellerSDK
import react_native_storyteller_sdk_gam
StorytellerGamSdk.registerAdRequestEnricher { requestInfo, request in
let bidderTargeting = await bidder.targeting(for: requestInfo)
var customTargeting = request.customTargeting ?? [:]
customTargeting.merge(bidderTargeting) { _, bidderValue in bidderValue }
request.customTargeting = customTargeting
}
The native callback may complete immediately or await host-owned bidder work. It mutates the prepared request in place and returns only when enrichment is complete; do not retain the request after the callback returns. Call StorytellerGamSdk.clearAdRequestEnricher() to remove the hook before a later setupGAMModule call. This is a native iOS API and is intentionally absent from the React Native JavaScript spec.
Storyteller AdMob Ads#
The GAM package also exposes AdMob setup for apps that use AdMob instead of Google Ad Manager. Only one Google ads module should be active at a time; call either setupGAMModule or setupAdMobModule.
import { setupAdMobModule } from '@getstoryteller/react-native-storyteller-sdk-gam';
setupAdMobModule({
nativeAdUnit: 'ca-app-pub-xxx/native',
bannerAdUnit: 'ca-app-pub-xxx/banner-fallback',
bottomBannerAdUnit: 'ca-app-pub-xxx/clips-bottom-banner',
enableBannerAdPriority: false,
customKvps: {
custom_key: 'custom_value',
},
});
Configuration parameters are explained below:
nativeAdUnit- Required AdMob ad unit ID used for native ads.bannerAdUnit- Optional AdMob banner ad unit ID used as a fallback for full-screen placements.bottomBannerAdUnit- Optional AdMob banner ad unit ID used for Clips bottom banner ads.enableBannerAdPriority- Optional boolean. Whentrue, banner ads are attempted before native ads whenbannerAdUnitis configured.customKvps- Optional key-value pairs passed to AdMob as network extras when ad tracking is enabled.
Types exported by the GAM package (import for type safety):
import type {
AdRequestPayload,
StoriesAdRequest,
ClipsAdRequest,
ItemInfo,
Category,
SetupGAMConfiguration,
SetupAdMobConfiguration,
} from '@getstoryteller/react-native-storyteller-sdk-gam';
Necessary part of the GAM Ads setup is implementing a listener that sets the ID of the Ad Unit in Google Ad Manager that will be used to serve the Storyteller Ads for the specific Ad request. Each ad request includes a unique requestId that must be returned with your response.
Basic Example#
import { useEffect } from 'react';
import type { EventSubscription } from 'react-native';
import StorytellerGamSdk, {
getAdUnitForRequest,
setAdUnitResult,
type AdRequestPayload,
} from '@getstoryteller/react-native-storyteller-sdk-gam';
const STORIES_AD_UNIT = '/YOUR-CONFIGURATION';
const CLIPS_AD_UNIT = '/YOUR-CONFIGURATION';
function MyComponent() {
useEffect(() => {
const subscription: EventSubscription = getAdUnitForRequest(
({ requestId, adRequest }: { requestId: string; adRequest: AdRequestPayload }) => {
// Determine which ad unit to use based on the request type
let adUnitId = '';
if (adRequest.stories) {
adUnitId = STORIES_AD_UNIT;
} else if (adRequest.clips) {
adUnitId = CLIPS_AD_UNIT;
}
// Return the ad unit ID with the matching requestId
setAdUnitResult({ requestId, adUnitId });
}
);
return () => subscription.remove();
}, []);
// ... rest of your component
}
const { GET_AD_UNIT_FOR_REQUEST } = StorytellerGAMAds.getConstants();
constructor(props: DemoAppProps) {
const storytellerEvent = new NativeEventEmitter(StorytellerGAMAds);
storytellerEvent.addListener(GET_AD_UNIT_FOR_REQUEST, this._onAdUnitRequested);
}
_onAdUnitRequested = (event: AdUnitRequest) => {
if(event.adRequest.stories) {
StorytellerGAMAds.setAdUnitResult({ adUnitId: '[storiesAdUnitId]' });
} else if (event.adRequest.clips) {
StorytellerGAMAds.setAdUnitResult({ adUnitId: '[clipsAdUnitId]' });
}
};
Advanced Example with Request Details#
You can access detailed information about the ad request to make more sophisticated decisions:
import { useEffect } from 'react';
import type { EventSubscription } from 'react-native';
import StorytellerGamSdk, {
getAdUnitForRequest,
setAdUnitResult,
type AdRequestPayload,
} from '@getstoryteller/react-native-storyteller-sdk-gam';
function MyComponent() {
useEffect(() => {
const subscription: EventSubscription = getAdUnitForRequest(
({ requestId, adRequest }: { requestId: string; adRequest: AdRequestPayload }) => {
let adUnitId = '';
if (adRequest.stories) {
const { placement, categories, story } = adRequest.stories;
console.log('Stories ad request:', {
placement,
categories,
storyCategories: story.categories,
adIndex: adRequest.stories.adIndex,
});
// Use different ad units based on placement or categories
adUnitId = '/YOUR_NETWORK_CODE/stories-native-ad-unit';
} else if (adRequest.clips) {
const { collection, clip } = adRequest.clips;
console.log('Clips ad request:', {
collection,
clipCategories: clip.categories,
nextClipCategories: adRequest.clips.nextClip?.categories,
adIndex: adRequest.clips.adIndex,
});
// Use different ad units based on collection or categories
adUnitId = '/YOUR_NETWORK_CODE/clips-native-ad-unit';
}
setAdUnitResult({ requestId, adUnitId });
},
);
return () => subscription.remove();
}, []);
// ... rest of your component
}
AdRequestPayload#
The AdRequestPayload object contains metadata about the ad request, including either stories or clips information.
// See package exports for exact type definitions
// Shown here for clarity only
type Category = {
name: string;
externalId: string;
};
type ItemInfo = {
contentId?: string;
categories: Category[];
};
type AdRequestPayload = {
stories?: {
placement: string;
categories: string[];
story: ItemInfo;
adIndex: number;
};
clips?: {
collection: string;
clip: ItemInfo;
nextClip?: ItemInfo;
adIndex: number;
};
};
Stories requests include:
- placement: string — uniquely identifies where Stories are shown
- categories: string[] — categories assigned to the stories list
- story: ItemInfo — metadata about the story after which the ad will be placed
- adIndex: number — ad position supplied by the native integration
Clips requests include:
- collection: string — collection identifier
- clip: ItemInfo — metadata about the clip for which the ad is requested
- nextClip: ItemInfo | undefined — metadata for the next clip when supplied by the native integration
- adIndex: number — ad position supplied by the native integration
ItemInfo.contentId is available on Android 11.6.3+ and iOS 11.7.0+ for core, GAM, and VAST dynamic request callbacks, including current and next Clips and Stories. The TypeScript field remains optional for compatibility.
Storyteller VAST Ads#
The VAST package exposes the native Storyteller VAST integration for apps that need VAST-backed full-screen Story or Clip ads. Install it alongside the core SDK:
npm install @getstoryteller/react-native-storyteller-sdk
npm install @getstoryteller/react-native-storyteller-sdk-vast
The VAST package exports its configuration, callback, diagnostics, URL-format, and request payload types:
import type {
SetupVASTConfiguration,
SetupGAMVASTConfiguration,
SetVASTRequestParametersResult,
VASTDiagnosticsEvent,
VASTURLFormat,
AdRequestPayload,
StoriesAdRequest,
ClipsAdRequest,
ItemInfo,
Category,
} from '@getstoryteller/react-native-storyteller-sdk-vast';
VASTURLFormat is 'pathSegment' | 'queryString'. SetupVASTConfiguration contains baseUrl, optional requestParameters, urlFormat, enableDebugLogging, and usesRequestParametersCallback. SetupGAMVASTConfiguration contains adUnit, descriptionUrl, and optional contentUrl, customParams, tagParameters, and enableDebugLogging.
Generic VAST#
Use setupVASTModule when your app already has a VAST tag endpoint and can provide request parameters directly.
import StorytellerVastSdk from '@getstoryteller/react-native-storyteller-sdk-vast';
StorytellerVastSdk.setupVASTModule({
baseUrl: 'https://pubads.g.doubleclick.net/gampad/ads',
urlFormat: 'queryString',
requestParameters: {
iu: '/21775744923/external/single_preroll_skippable',
sz: '640x480',
gdfp_req: '1',
output: 'vast',
unviewed_position_start: '1',
env: 'vp',
},
});
Configuration parameters:
baseUrl- Required HTTPS VAST tag endpoint.requestParameters- Optional static key-value parameters appended to each VAST request.urlFormat- Optional serialization mode. UsequeryStringfor normal?key=valuequery parameters orpathSegmentfor path-segment serialization. Defaults topathSegment.enableDebugLogging- Optional Android-only verbose VAST logging flag.usesRequestParametersCallback- Optional. Whentrue, native code emitsgetVASTRequestParametersForRequestand waits up to one second forsetVASTRequestParametersResult; if the response is late, the request continues without dynamic parameters.
The final VAST tag URL must be HTTPS and no longer than 2,048 characters after parameters are applied.
When usesRequestParametersCallback is enabled, subscribe before presenting Storyteller content and return a result with the same requestId:
import StorytellerVastSdk from '@getstoryteller/react-native-storyteller-sdk-vast';
const subscription = StorytellerVastSdk.getVASTRequestParametersForRequest(({ requestId, adRequest }) => {
StorytellerVastSdk.setVASTRequestParametersResult({
requestId,
requestParameters: {
content_type: adRequest.stories ? 'story' : 'clip',
},
});
});
// Later:
subscription.remove();
SetVASTRequestParametersResult contains requestId and optional requestParameters. Responses received after the native timeout are ignored for that request.
GAM VAST#
Use setupGAMVASTModule when you want the SDK to build Google Ad Manager VAST tag URLs without integrating Google Mobile Ads or IMA.
import StorytellerVastSdk from '@getstoryteller/react-native-storyteller-sdk-vast';
StorytellerVastSdk.setupGAMVASTModule({
adUnit: '/21775744923/external/single_preroll_skippable',
descriptionUrl: 'https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/tags',
tagParameters: {
sz: '640x480',
unviewed_position_start: '1',
},
});
Configuration parameters:
adUnit- Required GAM ad unit path serialized asiu.descriptionUrl- Required HTTPS page or content URL serialized asdescription_url.contentUrl- Optional HTTPS content URL serialized asurl.customParams- Optional custom targeting encoded intocust_params.tagParameters- Optional top-level GAM VAST tag parameters applied after SDK defaults.enableDebugLogging- Optional Android-only verbose VAST logging flag.
setupVASTModule and setupGAMVASTModule replace only the active VAST family, so VAST and GAM VAST remain mutually exclusive while supported GAM, AdMob, and Android IMA modules stay registered. This changes the previous iOS behavior where VAST setup replaced the complete Storyteller module list. Apps that require a VAST-only module set should explicitly disable the Google family first:
import { disableGoogleAdsModule } from '@getstoryteller/react-native-storyteller-sdk-gam';
import StorytellerVastSdk from '@getstoryteller/react-native-storyteller-sdk-vast';
disableGoogleAdsModule();
StorytellerVastSdk.setupVASTModule({
baseUrl: 'https://publisher.example/vast',
});
To remove VAST or GAM VAST without changing other ads modules, call disableVASTModule():
import { disableVASTModule } from '@getstoryteller/react-native-storyteller-sdk-vast';
disableVASTModule();
Disabling or replacing the VAST family completes pending request-parameter callbacks with empty parameters. Consumer-owned request and diagnostics subscriptions remain installed until the consumer removes them.
VAST Diagnostics#
The VAST package emits diagnostics separately from user activity analytics. Subscribe while testing or when collecting support data:
import StorytellerVastSdk from '@getstoryteller/react-native-storyteller-sdk-vast';
const subscription = StorytellerVastSdk.onVASTDiagnostics(event => {
console.log('VAST diagnostic', event.type, event);
});
// Later:
subscription.remove();
Event type values include requestStarted, requestCompleted, requestFailed, parseFailed, wrapperResolved, mappingFailed, and mediaSelectionFailed. Some fields are platform-specific; handle optional fields defensively.
VASTDiagnosticsEvent exports the complete optional diagnostics payload: slotId, baseUrl, urlFormat, parameterCount, totalMs, vastVersion, adCount, wrapperDepth, errorType, errorCode, httpStatus, cause, availableMimes, candidateCount, and message.
VAST modules support full-screen Story and Clip ads. Clips bottom banner ads are not part of the VAST protocol surface and are not supported by the VAST package.
Custom Ads#
If your tenant is configured for client-supplied ads, subscribe to StorytellerSdk.getAdsForList and complete each native request with either completeAdRequest(ad) or failAdRequest(error).
import { useEffect } from 'react';
import StorytellerSdk, { type StorytellerAd } from '@getstoryteller/react-native-storyteller-sdk';
function MyComponent() {
useEffect(() => {
const subscription = StorytellerSdk.getAdsForList(async adRequest => {
try {
const ad: StorytellerAd = await loadAdForRequest(adRequest);
StorytellerSdk.completeAdRequest(ad);
} catch (error) {
StorytellerSdk.failAdRequest(error instanceof Error ? error.message : 'No ad available');
}
});
return () => subscription.remove();
}, []);
}
StorytellerAd#
For type-safe action destinations, import the exported StorytellerAdActionType union or the runtime StorytellerAdActionKind enum:
import {
StorytellerAdActionKind,
type StorytellerAd,
type StorytellerAdActionType,
} from '@getstoryteller/react-native-storyteller-sdk';
const actionType: StorytellerAdActionType = 'web';
const runtimeActionType = StorytellerAdActionKind.web;
The StorytellerAd object which the SDK expects to be returned contains the following properties:
id: string- a unique ID for the Ad in question.advertiserName: string- the name of the advertiser - used in place of the Story title on the Ad Page.advertiserDescription?: string- optional advertiser description. This is consumed by iOS; the linked Android SDK image/video ad factories do not expose this field.advertiserLogoURL?: string- optional advertiser logo URL. This is consumed by iOS; the linked Android SDK image/video ad factories do not expose this field.image?: string- the image to display for the ad.video?: string- the video to display for the ad. Note that if both an image and a video are supplied, then thevideois preferred.playcardUrl?: string- the image to display as a placeholder if a video asset is still loading. It could be set, for example, as the first frame of the video. This property is unused for image ads.duration?: number- how long the ad should be displayed for. Note that for videos, this parameter will be ignored and the ad will be displayed for the length of the video. If this parameter isnullfor animagepage, it will default to 5s.trackingPixels: StorytellerAdTrackingPixel[]- an array of 1x1 ad tracking pixels which will be triggered when an ad is loaded and throughout its playback - properties detailed below.action?: StorytellerAdAction- optional property which describes what should happen when a user presses an action button on the ad. Omit this property when no action is required.adFormat: string- format identifier. This is required by iOS; the linked Android SDK image/video ad factories always create custom native ads.responseIdentifier?: string- optional response identifier. This is consumed by iOS; the linked Android SDK image/video ad factories do not expose this field.
The StorytellerAdTrackingPixel object contains the following properties:
eventType: string- string describing the type of the tracking pixel - possible values are "impression", "videoStart", "firstQuartile", "midpoint", "thirdQuartile", "videoComplete", "videoPause", "videoResume".url: string- the tracking pixel URL.
The StorytellerAdAction object contains the following properties:
type: StorytellerAdActionType- action destination. Possible values are:web(will direct the user to an in-app browser);inApp(will direct the user to a location within the integrating app);store(will direct the user to the App Store);externalApp(will direct the user to another App if the user has it installed)urlOrStoreId: string- string describing the destination. Forwebtypes, this should be a valid HTTPS URL. ForinApptypes, this should be a valid deeplink into the integrating app - when a user presses an action button on an ad we will calluserNavigatedToAppon the component and pass this URL for your app to direct the user to the correct destination. Forstoretypes, this should be the URL of the App you wish to link to on the App Store (e.g. https://apps.apple.com/us/app/testflight/id899247664)text?: string- string describing text on the bottom of the Story or Clip that suggests the available action. Default value isLearn More.
Ad Encoding#
Any video ads passed to the Storyteller SDK should be encoded according to the following specs:
- Width: 540
- Height: 960
- Bitrate: 1500 kbps
- MaxBitrate - 1500 kbps
- FrameRate - 30 fps
- Audio: AAC 128 kbps bitrate, 48k sampling
Android 12 Clips cadence overrides#
StorytellerClipsAdConfiguration accepts optional Android-only frequency and initialIndex values on native 12.0.0. They apply to the selected presentation's eligible standard between-Clips ad strategy; they cannot enable ads or bypass consent, provider or remote eligibility gates. They do not change IMA pre-roll or bottom banners.
frequency: positive integer interval. Omitted, null or non-positive values inherit remote frequency.initialIndex: non-negative integer.0is the special opening-ad value; positive values are one-based content positions (2means after the second Clip). Omitted, null or negative values inherit the remote initial index.
Pass these fields alongside the existing configuration flags when opening a collection or configuring a Clips view:
adConfiguration: {
preRollEnabled: true,
bottomBannerEnabled: false,
frequency: 3,
initialIndex: 2,
}
The current iOS bridge ignores these two cadence fields. Existing omitted boolean behavior is unchanged.