This blog post provides an example of implementing an orchestration process in Orchex that applies audience segmentation, personalization, and simplification to data retrieved from the Contentstack content management system.
Update 15.Dec.2025: After years of frustration with WordPress, I am finally abandoning this blog. The content will likely stay here for some time, but new content will appear here:
We have a content type in Contentstack that has a modular blocks field that contains two nested fields: one to select audience segments for targeting the content and another to contain HTML content. The data looks like this:

We need to retrieve and transform entries, sorting content blocks associated with any audience segments associated with visitor first, and then personalizing that content using handlebars templates. Orchex uses a rust implementation of handlebars – JavaScript running in a server-side JVM invokes a Rust function.
First, we need a couple of settings to access Contentstack:
{
"description": "Contentstack delivery token.",
"dontexportkey": false,
"dontexportvalue": true,
"encrypted": false,
"key": "demo-contentstack-access_token",
"valueperenv": true,
"values": ["<replace with your Contentstack delivery token>"]
}
./json/settings/demo/contentstack/access_token.json
{
"description": "Contentstack stack identifier.",
"dontexportkey": false,
"dontexportvalue": true,
"encrypted": false,
"key": "demo-contentstack-api_key",
"valueperenv": true,
"values": ["<replace with your Contentstack stack identifier>"]
}
./json/settings/demo/contentstack/api_key.json
I don’t have a CDP, so we’ll just fake the CDP for now, but data like this would normally come from a processor that invokes a webservice API to get data from a CDP, or otherwise.
{
"key": "demo-cdp",
"postrunscript": {
"block_type": "json_inline",
"content": {
"name": "John West",
"email": "unique@uniquely.unique",
"segments": [ "technology", "customers", "developers", "marketers" ]
}
}
}
./processors/demo/cdp.json
Then, we need a processor template to store connection details:
Update: This JSON fragment is obsolete. All properties relevant to webservice APIs have moved under a root webapi key.
{
"key": "demo-contentstack-template",
"method": {
"block_type": "literal",
"content": "GET"
},
"protocol": {
"block_type": "literal",
"content": "https"
},
"domain": {
"block_type": "literal",
"content": "cdn.contentstack.io"
},
"headers": [
{
"name": "api_key",
"logic_block": {
"block_type": "js_inline",
"content": "get_setting('demo-contentstack-api_key');"
}
},
{
"name": "access_token",
"logic_block": {
"block_type": "js_inline",
"content": "get_setting('demo-contentstack-access_token');"
}
},
{
"name": "Content-Type",
"logic_block": {
"block_type": "literal",
"content": "application/json"
}
}
]
}
./json/processors/demo/contentstack/template.json
Then, we need a processor that inherits those values and retrieve the entry specified in the payload. The JavaScript here says to set the response to include a key named “response” with the result of the API call and optionally another named debug that would contain information to help diagnose issues with orchestration processes.
{
"key": "demo-contentstack-entry",
"inherits": "demo-contentstack-template",
"rootpath": {
"block_type": "js_inline",
"content": "result = '/v3/content_types/' + orchex_payload.content_model + '/entries/' + orchex_payload.entry_id;"
},
"content": "
let response = { response: orchex_response };
if (orchex_context.debug) {
response.orchex = { context: orchex_context };
}
result = response;
"
},
"cachingoptions": {
"cacheable": true,
"cache_key_script": "result = orchex_payload;"
}
}
./json/processors/demo/contentstack/entry.json
This processor actually works.
curl -X POST http://127.0.0.1:3030/processors/demo-contentstack-entry/invoke -H "Content-Type: application/json" -d '{"orchex": {"debug": false, "verbose": false}, "content_model": "default_content_model",
"entry_id": "blte1d90ab36b963f9f"}' | jq

Now we need a processor to do the audience segmentation and personalization as well as entry reduction.
{
"key": "demo-contentstack-personalized",
"inherits": "demo-contentstack-entry",
"postrunscript": {
"block_type": "js_inline",
"content": "
function restructureEntry(entry, cdp) {
const segmentOrder = cdp.segments;
function getPriority(block) {
const segments = block.content_block.audience_segments;
if (!segments.length) return Infinity;
return Math.min(...segments.map(seg => {
const index = segmentOrder.indexOf(seg);
return index === -1 ? segmentOrder.length : index;
}));
}
entry.modular_blocks_field.sort((a, b) => getPriority(a) - getPriority(b));
}
cdp = orchex_context.orchex_processors['demo-cdp'].output_json;
renderedEntry = JSON.parse(JSON.stringify(orchex_context.orchex_processors['demo-contentstack-entry'].output_json.response.entry));
blocks = renderedEntry.modular_blocks_field;
for (i = 0; i < blocks.length; i++) {
block = blocks[i];
if (block.content_block && block.content_block.content_field) {
pass = JSON.stringify({
template: JSON.stringify(block.content_block.content_field),
data: cdp
});
block.content_block.content_field = render_template(pass);
}
}
keysToRemove = [
'ACL',
'_in_progress',
'_version',
'created_at',
'created_by',
'locale',
'publish_details',
'updated_by'
];
keysToRemove.forEach(key => renderedEntry[key]);
restructureEntry(renderedEntry, cdp);
let response = { response: renderedEntry };
if (orchex_context.debug) {
response.orchex = { context: orchex_context };
}
result = response;
"
},
"cachingoptions": {
"cacheable": true,
"cache_key_script": "result = { 'a': orchex_payload, 'b': orchex_context.orchex_processors['demo-cdp'].output_json.email };"
}
}
result = response;
./json/processors/demo/contentstack/personalized.json
We can see that it has ordered the blocks for developers first and personalized the content:
curl -X POST http://127.0.0.1:3030/processors/demo-contentstack-personalized/invoke -H "Content-Type: application/json" -d '{"orchex": {"debug": false, "verbose": false}, "content_model": "default_content_model",
"entry_id": "blte1d90ab36b963f9f"}' | jq | grep content_field

Caching options specify that this is cached at a user level, which may consume memory and may not be worthwhile. For better caching, we should implement this process as two separate processors: first call demo-content-entry to retrieve and cache the entry and then use that in demo-content-personalized.
{
"key": "demo-contentstack-personalized",
"inherits": "demo-contentstack-entry",
"postrunscript": {
"block_type": "js_inline",
"content": "
cdp = orchex_context.orchex_processors['demo-cdp'].output_json;
entry = orchex_context.orchex_processors['demo-contentstack-entry'].output_json.response.entry;
function restructureEntry(entry, cdp) {
const segmentOrder = cdp.segments;
function getPriority(block) {
const segments = block.content_block.audience_segments;
if (!segments.length) return Infinity;
return Math.min(...segments.map(seg => {
const index = segmentOrder.indexOf(seg);
return index === -1 ? segmentOrder.length : index;
}));
}
entry.modular_blocks_field.sort((a, b) => getPriority(a) - getPriority(b));
}
blocks = entry.modular_blocks_field;
for (i = 0; i < blocks.length; i++) {
block = blocks[i];
if (block.content_block && block.content_block.content_field) {
pass = JSON.stringify({
template: JSON.stringify(block.content_block.content_field),
data: cdp
});
block.content_block.content_field = render_template(pass);
}
}
keysToRemove = [
'ACL',
'_in_progress',
'_version',
'created_at',
'created_by',
'locale',
'publish_details',
'updated_by'
];
keysToRemove.forEach(key => entry[key]);
restructureEntry(entry, cdp);
let response = { response: entry };
if (orchex_context.debug) {
response.orchex = { context: orchex_context };
}
result = response;
"
},
"cachingoptions": {
"cacheable": true,
"cache_key_script": "result = { 'a': orchex_payload, 'b': orchex_context.orchex_processors['demo-cdp'].output_json.email };"
}
}
./json/processors/demo/contentstack/personalized.json (alternate version with dependency and caching)
One thought on “-Personalization and Audience Segmentation with Orchex and Contentstack”