1Why we did not replace anything
We run a large martech stack. Sitecore for the web, a DAM for the assets, and the usual crowd of analytics, CRM and campaign tools around them. Once a year someone senior asks whether we should replace one of the pieces.
The answer is almost always no. Replacing a platform is never just a licence swap. You pay for migration, retraining, integration rework, a slower delivery quarter, and a long tail of small things nobody put in the business case. Extending a platform you already own costs a fraction of that.
So the real question is not "is there something better out there". It is "is this vendor's roadmap clear enough that I am happy building on top of it". If yes, extend. If no, start the replacement conversation properly.
For Sitecore the answer was yes. The Marketplace extension model is clearly where they are investing, which means anything I build on it improves over time instead of becoming a customisation I have to defend at every upgrade.
2What we needed to build
This summer the question landed on my desk in a smaller shape. Our authors were downloading images from Brandworkz, our DAM, and uploading them into Sitecore by hand. Every upload made a second copy of an asset the brand team thought they controlled. Nobody could say where a given image was used. Expired licences stayed live on the site because nothing in Sitecore knew a licence existed.
I looked at three routes.
Buy the vendor's connector
Some DAM vendors publish one. Bynder does. Ours does not. That closed the option in ten minutes, though I still spent a day studying Bynder's connector, because it is the closest thing to a reference implementation the market has. That day paid for itself later.
Sync the DAM into the Media Library
Copy assets across on a schedule and let authors pick them normally. It demos well. Then you own a sync engine, a storage bill, two systems that both think they own the asset, and a stale-copy problem that grows every month. Cheap in a slide, expensive in year two.
Build our own Marketplace app
Write the app, host it, register it in our Cloud Portal, and let Sitecore load it where authors need it. More work up front, but we own the behaviour and the data shape, and the DAM stays the single source of truth.
I picked the third one. A useful detail if you go the same way: a Marketplace app does not have to be public. You can register it as a custom, organisation-scoped app inside your own Cloud Portal. No review, no listing, no approval cycle. Publishing it later is a new App ID over the same code.
3The architecture came first
Everything else in this article depends on this section. Before a single screen was drawn I wrote down who the parties are, which one is allowed to talk to which, what gets stored where, and which parts I expect to change later. An hour of that saved weeks, and it is the part an AI agent cannot do for you, because it is all judgement and no typing.
Here is the whole integration on one page.
The four runtime parties
| Party | Role |
|---|---|
| XM Cloud (Pages) | Loads the connector in iframes at registered extension points. Stores the field value. Never stores a binary. |
| The connector app | The authoring UI plus a server-side API layer. Owns all secrets. The only party that talks to the DAM or to the Sitecore Authoring API. |
| The DAM | System of record for assets and metadata. Provides search, the folder taxonomy, asset detail and delivery URLs. |
| The head application | The public site. Reads the field value, emits image, video or link tags pointing at DAM delivery URLs. No DAM calls at render time. |
Two parts I expected to change, so I isolated them
This is the highest-value paragraph in the architecture. Two surfaces were outside my control, so each one lives behind exactly one module. When either changes, one file changes and nothing else notices.
The Marketplace SDK
Pre-1.0 and still moving. All of it sits behind one folder: one hook that starts it up with a timeout, and one module that is the only caller of read, write and close. Nothing else in the app imports the SDK.
Public URL resolution
How a DAM produces a permanent public link differs by vendor, by licence and sometimes by tenant. All of that logic lives in one file, and it is built to fail loudly rather than store a link it cannot stand behind.
The third isolation is the picker itself, which never imports the SDK at all. It talks to a small interface instead, and that decision is worth its own section later, because it ended up serving five different surfaces without forking.
4What Sitecore actually stores
There are two ways to answer this, and they lead to two completely different products.
Copy the asset. For every selection, create a media item in Sitecore that mirrors the DAM record. You get "where used" for free, plus Content Editor support and search indexing. You also get a sync engine, two owners for one asset, and a lifecycle mess.
Reference the asset. Store a small JSON object in the field. No file ever enters Sitecore.
I checked what the mature commercial connector does before deciding, and it settled the argument. Bynder does not create per-asset media items and does not sync the DAM. It stores JSON in the field and ships two supporting pieces around it: base templates and a rendering package. If the vendor with the most experience of this problem chose the reference model, the burden of proof is on the other option.
So this is the value that lands in the field:
{
"schemaVersion": 1,
"provider": "brandworkz",
"assetGuid": "d76e2bc0-7933-4cc2-b4b0-3b8183279758",
"type": "Image",
"title": "spring-hero-mountains",
"publicUrl": "https://cdn.example-dam.net/.../spring-hero_1920.jpg",
"thumbnailUrl": "https://cdn.example-dam.net/thumbs/.../320x320.jpg",
"rendition": "web-large-1920",
"width": 7016,
"height": 4961,
"alt": "Hiker looking over a spring mountain valley",
"folderPath": "Brand/Campaign 2026",
"expiryDate": "2027-01-31",
"keywords": ["spring", "outdoor"],
"selectedAtUtc": "2026-07-07T14:05:00Z",
"selectedBy": "author@example.com"
}
Multiple assets wrap in a versioned envelope. My first draft used a plain { assets: [...] } wrapper and I caught it in review. A wrapper with no version number defeats the point of having a version number.
Rich text is the one exception. Those fields store plain sanitised HTML, exactly like Sitecore's own rich text field, not this JSON. More on why in section 15.
Four rules that keep the format honest
- One writer. Only one function ever produces a value, and it validates before it writes. A half-written value is not possible because no other code path can create one.
- Read defensively. Every read goes through a schema check that never throws. It returns one of five states: empty, one asset, several assets, unreadable, or a version this build does not know. The last two send the author to a recovery screen with the raw value visible and untouched.
- Adding optional fields is free. When we added a keywords snapshot, nothing broke and no version bump was needed. Removing or changing a field means a version bump and a conversation with the migration team.
- The build checks the paperwork. If the format code changes without the format document changing in the same commit, the build fails.
That last rule matters more with an AI in the loop than without one. An agent will happily change a schema and move on. A build rule will not let it.
5How the fields get built in Sitecore
This is the part most write-ups skip, and it is the part a content architect actually has to live with. A connector is not finished when the picker works. It is finished when someone can add a DAM field to a template in two minutes without typing a GUID.
The field type, and the one rule about Source
A DAM field in Sitecore is an ordinary template field with two settings:
Type: Marketplace Types → Plugin
Source: the connector's App ID, and nothing else
That second line is not a style preference. The Source must be the bare App ID. My design had per-field settings appended as query parameters, which seemed obvious. Do that and Pages silently fails to resolve the app: the field renders as a plain text area, the "Open app" button disappears, and no error appears anywhere. I spent an hour convinced I had broken the deployment.
Where per-field settings actually go
Settings live in the field's standard values, which is also what Bynder does. It works because of a small piece of platform behaviour you can lean on: an empty field returns its standard value when the app reads it. So the standard value holds a short settings string, and the app treats it as "empty field, configured like this" rather than as content.
| Setting | What it does |
|---|---|
types=Image | Restrict which asset types the author can pick. Values are case-sensitive. |
mode=single|multi | One asset or several. |
max=N | Cap the number of assets in multi-select. |
folder=<id> | Lock browsing to one part of the DAM tree, so a campaign field only ever shows campaign assets. |
rendition=<name> | Force a default image size, so authors cannot accidentally place an 8000px original. |
search=<term> | Pre-fill a search so the field opens on something useful. |
ui=richtext | Render the rich text editor instead of the picker. |
Because settings and content share one string, the app decides what to show from the shape of what it reads, and the order of those checks matters.
Base templates, so nobody types a GUID
Telling an architect "create a plugin field, set the Source to this ID, and put a settings string in standard values" is a support ticket waiting to happen. So the connector ships ready-made base templates under its own folder in the template tree. Each one carries a single, correctly wired field. A site template inherits the matching base and the field arrives working.
| Base template | Field | Behaviour |
|---|---|---|
BaseImage | BrandworkzImage | One image, images only |
BaseVideo | BrandworkzVideo | One video |
BaseAudio | BrandworkzAudio | One audio asset |
BaseDocument | BrandworkzDocument | One document |
BaseSingleSelect | BrandworkzAsset | One asset of any type |
BaseMultiSelect | BrandworkzAssets | Several assets, stored in the envelope |
BaseRichText | Content | The rich text editor with the DAM image button |
Settings | PortalUrl, Defaults | Non-secret configuration. Never credentials. |
These are delivered two ways: as serialised items you can commit and deploy with the rest of your content, or pushed straight into an environment by a script that can be run repeatedly without making a mess. Both come from the same generator, so there is one definition of the truth and no wiki page to go stale.
How an architect actually uses them
- One DAM field on a template: inherit the matching base. Done in a minute, nothing to configure.
- Two or more DAM fields on one template: inheritance cannot give you the same field name twice, so add the plugin fields by hand and copy the Source and standard-value pattern from a base.
- Name fields for humans. "Hero Image (Brandworkz)" tells an author what will open. "Asset1" does not.
Shipping a component has two halves
If you also ship droppable components that use these fields, plan for two deployments, not one. The Sitecore half is the datasource template, the rendering item and its place in the palette, all of which a script can push. The website half is registering the component name in the head application and deploying it.
Pages disables a palette tile until the editing host knows the component name, and there is no placeholder to fall back on. I found this out by scripting a perfect item deployment into a site where nothing could actually be added. Land both halves together.
6Six months we did not have
So the architecture was settled. Now price it the traditional way and you get two quarters. Wireframes and sign-off. A design system. A front end. A server layer. Templates. Renderers. An admin screen. Tests. Documentation. Nobody was going to fund six months of that to solve an image-copying problem.
So I did not run it as a traditional project. I ran it as an AI-led build, where I stayed on the decisions and an AI coding agent did the typing. It went from an empty repository to a working end-to-end connector in a handful of days of build time, and the git history still shows it: first commit to a verified insert inside Pages, inside one working week.
I want to be precise about what that does and does not mean, because there is a lot of noise about this right now.
What it changed
The cost of producing code, tests, scripts and documentation dropped to almost nothing. I could try an approach, see it working, and throw it away the same afternoon.
What it did not change
Everything in sections 3, 4 and 5 was still mine to decide. And every vendor limitation still had to be discovered by hand. No agent will tell you that a query string silently breaks your field. Only a live test does that.
That is the honest split. The architecture is the thinking. The build is the typing. Only one of those got faster.
7How I set up the AI workflow
Half a day, no product code. An AI agent with no rules produces a large pile of plausible code that nobody can maintain. Four things did most of the work.
A rules file the agent reads every session
One document at the root of the repo, mirrored under the filenames different AI tools look for, so the same rules load whichever tool I use that day. It holds the project context, the architecture decisions from sections 3 to 5, the things we will not build, the security rules and the quality bar. It reads like instructions to a new mid-level engineer, because that is roughly the right level. When something in it turns out to be wrong, I update it and every future session inherits the correction.
Folders that keep four kinds of work apart
This sounds like housekeeping and turned out to be the biggest single quality lever:
| Folder | What lives there |
|---|---|
| inputs | Things I provide: designs, vendor emails, exports. The agent reads them and never edits them. |
| directives | Procedures in plain English. Registering the app, credentials, tunnelling for local development, release. |
| execution | Small, commented scripts that do repeatable things. Smoke tests, format validation, link checks, template deployment. |
| knowledge | The project's memory. Decision log, the data format, verified API behaviour, and a file of things that cost more than fifteen minutes. |
| app / src | The product. Reviewed code only. No experiments, no dead code, no half-finished stubs. |
| .tmp | Every scratch file: API dumps, throwaway scripts, generated samples. Never committed, always safe to delete. |
The rule is that shipped code never imports from scratch folders, and scratch work never lands in product folders. A clean repository at the end of every session means nothing is in the wrong place. It sounds fussy. It is the difference between a codebase you can hand over and a demo you have to rewrite.
Push repetition into scripts, keep judgement with me
Anything I would otherwise check by hand became a script. Do not eyeball a CDN URL, run the URL checker. Do not read JSON to see if it is valid, run the validator. Do not click through the DAM to see if credentials still work, run the smoke test. Language models are probabilistic and errors compound across steps. Scripts are not. Every check I moved into code made the next twenty sessions more reliable.
Every failure gets written down before it is closed
This is the part I would keep even if I went back to writing everything by hand. When something broke, the fix was not finished until five things had happened:
- Reproduce it with the smallest repeatable thing. A failing test or a script, never a manual click-through.
- Fix it.
- Confirm the failing case passes and the suite is still green.
- Write down what was learned, the same day, in the knowledge folder.
- Add a test or extend a script so it cannot come back quietly.
The effect compounds. By week two the agent was starting each session by reading a file of verified API quirks it had discovered itself, so it stopped repeating the same mistakes. Most of this article came out of those notes.
8Step 1: Draw the screens
With the architecture written down, the build starts with pictures. Not because design comes first as a principle, but because a wireframe is the cheapest place to have an argument. Changing a picture takes a minute. Changing a built screen takes an afternoon and an opinion about whether it was worth it.
I described the journeys in plain English: an author opens an empty field, searches, opens an asset, adds alt text, inserts it, and sees a card. Then the failure paths, which are the ones people forget: no results, no connection, an expired asset, a value nobody can read.
That produced these eight wireframes. Between them they cover every surface the connector ended up shipping, so they are also the quickest way to see what the product actually does.
Reading them back months later, the useful thing is how little changed. The picker shipped almost exactly as drawn. The one screen that moved was the rich text editor, which did not exist yet at this point and arrives in section 14 after a fight with the platform.
My job
Describe the journeys and the failure paths. Decide what an author must not be allowed to do, like inserting an image with no alt text.
The AI's job
Turn those journeys into labelled wireframes and a numbered requirement for every behaviour, so nothing gets built that nobody asked for.
One rule made everything downstream easier: every screen got an ID and every requirement got a number. From then on, work was either traceable to one of those or it did not get built. That is how you stop an eager agent adding features nobody wants, and it is how you keep a small project honest.
9Step 2: Build the design system
The app runs inside Sitecore, so it has to look like it belongs there. Sitecore publishes its own design system, Blok, and using it is the difference between an app that feels native and one that feels bolted on. That decision took ten minutes and saved weeks.
Later we wanted the connector to carry our own brand too, which is where most teams either fork the design system or start overriding styles screen by screen. Instead the brand became a thin layer on top: swap one colour scale, keep everything else. Because every button and tab already referenced the primary colour by name rather than by hex code, changing one file rebranded the whole product.
Two details worth copying. Status colours were left alone, because success and error must never look like brand colours. And the brand values live in a plain constants file separate from the theme, because theme code only runs in the browser and server-rendered pages needed the same values. That second one cost me half an hour before I understood it, which is exactly the kind of thing that ends up in the gotchas file.
My job
Use the host's design system, and treat branding as a layer rather than a replacement.
The AI's job
Wire the theme, build the shared header and logo as self-contained components, and apply them consistently to every screen.
10Step 3: Build the picker on its own
Our XM Cloud instance was empty when I started. Waiting for it would have wasted a week, so I built the picker as a normal web app on its own URL, with no Sitecore in the picture at all.
That turned out to be one of the better calls. I could develop with hot reload instead of inside an iframe in someone else's app. I could demo on day two. And it became a real feature, because people who never open Sitecore can use it to browse the DAM and copy a public link.
What went into it, in the order authors care about:
Search that keeps up
Typing is debounced, there is a two-character minimum, and paging is real.
The real folder tree
Loaded a level at a time, with expand arrows only where there are children.
Type filters
Image, video, document, audio, and a field can lock them down.
Asset detail
Large preview, metadata, keywords, size choice, and a link back into the DAM.
The alt text gate
You cannot insert an image without alt text. The only moment anyone will type it.
Expiry and link health
If a licence is running out or a link has gone bad, the author sees it. Never silently.
Keyboard navigation and a focus trap went in with the layout, not afterwards. Retrofitting accessibility into a grid of cards is miserable, and the accessibility checks run in the tests so it stays fixed.
This is also where the third architectural isolation from section 3 gets built. The picker never talks to Sitecore directly. It talks to a small interface, and I wrote two implementations of it: one that writes to a Sitecore field, one that copies to the clipboard.
export interface SelectorHost {
readonly kind: "sitecore" | "standalone" | "rte";
readonly capabilities: { canWriteField: boolean; canClose: boolean };
readValue(): Promise<string>;
// sitecore -> write to the field, wait, close
// standalone -> copy the JSON, stay open
// rte -> hand back an <img> tag
// Must throw and change nothing if it fails.
commitSelection(json: string): Promise<void>;
writeValue?(json: string): Promise<void>; // save without closing
cancel(): Promise<void>;
}
Three to five days to put that in before the UI work, against about two weeks to retrofit it. It has paid back repeatedly: the same picker now serves the field inside Pages, the standalone browser, the rich text editor, the clipboard helper and the standalone field editor. Five surfaces, one picker, no forks.
11Step 4: Connect the real DAM API
The server side is small and boring on purpose. Every route wraps its work in one shared handler that assigns a correlation ID, applies rate limiting, and maps errors onto a short list of categories, so no raw exception or upstream payload ever reaches the browser. The Sitecore routes additionally require an admin key.
| Route | What it does |
|---|---|
/search | Search proxy: query, type filter, folder scope, paging. Returns clean data, never the raw response. |
/asset/[id] | One asset's detail. |
/asset/[id]/resolve-url | The permanent public URL. The isolated seam from section 3. |
/folders | The folder tree, one level at a time. |
/bootstrap | Non-secret facts the UI needs, like the portal URL for deep links. |
/health | Reachability plus a categorised diagnosis. A deep mode also runs a real search. |
/link-check | Checks a stored URL still works. Restricted to known DAM hosts so it cannot be used to probe your network. |
/sitecore/* | Item read and write, tree browse, and the usage report, through the Authoring API. Admin key required. |
Now the part that matters if you are working with an AI agent. The documentation for an API and the actual behaviour of your tenant are two different things, and a language model has only ever read the documentation. So the first thing built was not a feature. It was a script that hits the live API with real credentials and prints exactly what comes back. In one afternoon it found four problems that would each have cost days later.
My job
Supply the credentials, run the script against the live tenant, and read the output properly instead of assuming.
The AI's job
Write the probe script, then turn each finding into working client code, a note in the knowledge file, and a test.
Trap 1: HTTP 200 does not mean it worked
Every response is wrapped: { result: { code, message }, ... }. Code 0 means success. Code 1 means "you are not logged in", and it arrives with a cheerful HTTP 200. The fetch wrapper checks the envelope, not the status line.
Trap 2: the token that authenticates nothing
I got a valid bearer token from the token endpoint. The REST endpoints rejected it. They want a session cookie from a separate integration login, plus load balancer cookies that rotate on every response and have to be carried forward or the session dies mid-browse. I found this by reading the JavaScript on the vendor's own Swagger login page.
Trap 3: case-sensitive IDs
One endpoint hands you a folder ID in uppercase. The search index stores IDs in lowercase and matches case-sensitively. Filter with the ID you were just given and you get zero results, no error, forever. Every identifier is now lowercased at the boundary, with a comment saying why, because it looks redundant until you know.
Trap 4: the endpoint that looks right and is not
My first folder tree showed one empty folder while the DAM portal showed hundreds. I was listing folder-shaped assets from the search index instead of the actual folder structure, which lives behind a completely different endpoint. Two more surprises on the same trail: a third endpoint looks perfect for browsing but returns no asset IDs, so it cannot build my JSON at all, and a folder-scoped search returns an empty list with a correct total unless you also send a search term. Browsing now sends a wildcard.
12Step 5: Put it inside Sitecore
With the picker working, the API mapped and the field architecture already decided, embedding was mostly configuration. Register the app, point the extension points at the deployed URL, create the plugin field, set the standard value, open a page. My app appeared inside the field. That was a good afternoon.
Then I hit the thing I would warn every reader about.
To save a value you call setValue() and then closeApp(). The catch is that setValue resolves when the host acknowledges the message, which can happen before the value is stored. Close on that acknowledgement and you have built a connector that occasionally loses an author's work: rarely enough that nobody can reproduce it, often enough that people stop trusting it.
The fix is boring. Acknowledge, wait, then close. It lives in exactly one file, with a comment explaining why the number is there so nobody tidies it away later.
// The only place in the app that reads or writes a field value.
// setValue resolves on acknowledgement, which can arrive before the
// value is actually stored. Do not remove the guard delay.
export const SET_VALUE_GUARD_MS = 500;
export async function writeFieldValue(client: ClientSDK, json: string) {
await client.setValue(json, true);
await new Promise((r) => setTimeout(r, SET_VALUE_GUARD_MS));
}
// Save and close. Throws WITHOUT closing if the save fails, so the field
// is never left half-written.
export async function writeFieldValueAndClose(client: ClientSDK, json: string) {
await writeFieldValue(client, json);
await client.closeApp();
}
Notice what happens on failure. It throws and does not close. The modal stays open, the author sees an error they can act on, and whatever was in the field before is untouched. That behaviour is the difference between a connector people trust and one they work around.
13Step 6: Test as you go
Tests are where AI-led development either works or falls apart. Generated code looks convincing, so you need something other than your own reading to tell you whether it is right. Tests were written with each feature, never afterwards, and the rule was simple: a feature without tests was not finished.
| Layer | What it protects |
|---|---|
| Unit | The pure logic. Building and reading the JSON, the API mapping, the sanitiser, the image markup, error handling. |
| Component | The picker and the renderers, with accessibility checks on every screen. |
| Journey | Browser tests of the real app: search to insert with the alt text gate, the admin screen, and what happens when the app is opened outside Sitecore. |
| Smoke | Scripts that hit the live DAM and my own API. These are what catch the vendor changing something. |
That is currently 87 unit and component tests across 14 files, three browser journeys, and a handful of scripts. The whole unit suite runs in about two seconds, which is the only reason anyone actually runs it. Every gate runs in CI in the same order every time: lint, type check, test, build.
14Adding images inside rich text
Then came the requirement I should have seen coming. Authors do not only put images in image fields. They put them inside body copy. This part taught me the most, so here are all four attempts.
Attempt 1: add a button to the editor toolbar. The obvious answer. I spent two days on it and the answer was no. The Marketplace SDK has no rich text extension point at all. The classic editor profiles can be edited through the API, but a custom button needs server-side JavaScript in the web root, and XM Cloud does not allow custom server code. You can create the button. It will do nothing when clicked.
Attempt 2: use the clipboard. Author picks in my full screen app, I build the markup and copy it, author pastes into the editor. This worked, and it proved something I needed to know: the editor kept all my custom data attributes through a paste, so the governance survives. But two clicks and a paste is a workaround, not a feature.
Attempt 3: reuse Sitecore's own DAM hook. There is a separate integration that loads an external search page in an iframe and inserts whatever it sends back. I chased it for a while before establishing that the Pages editor's image button goes to the Media Library instead, and that reusing the mechanism had licensing implications I did not want.
Attempt 4: stop asking. If I cannot put my button in their editor, I ship my own editor. The custom field is already an extension point, so I added a rich text mode to it, switched on by one setting from section 5. It has an editing surface, a toolbar matching Sitecore's own structural set, a source view, and one extra button that opens the same picker through the same interface from step 3.
Two storage decisions made attempt 4 survivable. First, the field stores plain sanitised HTML, exactly like Sitecore's own rich text field, not a JSON wrapper. My first version used a wrapper and I threw it away, because it blocked source-level editing and would have made migrating old HTML painful. Now legacy HTML pastes straight in and the source view edits it raw.
Second, inserted images carry their own identity:
<img src="https://cdn.example-dam.net/.../hero_1920.jpg"
alt="Hiker looking over a spring mountain valley"
width="1920" height="1080" loading="lazy"
data-bw-provider="brandworkz"
data-bw-asset-guid="d76e2bc0-7933-4cc2-b4b0-3b8183279758"
data-bw-rendition="web-large-1920"
data-bw-schema="rte-1" />
Those attributes are the whole governance story for inline images. Browsers ignore them, and the usage report in section 17 finds every DAM image in the content tree by scanning for them. They also version separately from the field JSON, because markup and data change for different reasons.
15Showing assets on the live website
A connector that only writes JSON into a field is half a product. Somebody still has to turn that JSON into an image on the page, and if you leave that to each front-end team you have shipped a bug generator. So the connector also ships a small package for the website: a reader and a set of React components.
import { parseBrandworkzField, BrandworkzImage } from "@brandworkz/sitecore-renderers";
export function Hero({ fields }) {
const parsed = parseBrandworkzField(fields.heroAsset?.value);
// One function, every stored shape, never throws:
// { kind: "asset", assets: [...] }
// { kind: "richtext", html: "..." }
// { kind: "empty" }
if (parsed.kind !== "asset") return null;
return <BrandworkzImage asset={parsed.assets[0]} sizes="100vw" />;
}
Three things make it worth packaging instead of copy-pasting. It never throws, so a DAM problem cannot take down a page. It has no dependencies beyond React, so it runs anywhere. And it stays tolerant on purpose: it reads the current format, the multiple-asset envelope, plain rich text HTML, and an older wrapper format I stopped writing months ago. Backward compatibility is the package's job, not the caller's.
Notice what is not happening here. The page renders from the URL stored in the field. There is no call to the DAM at render time, which means the website keeps working normally even if the DAM API is having a bad afternoon.
16Finding where each asset is used
This is the one honest weakness of storing references instead of copies. Without media items, Sitecore's built-in "where used" knows nothing about DAM assets. The brand and legal teams care far more about that than about how nice the picker looks.
So I built the answer directly. A report that walks the content tree through the Authoring API, finds every field holding one of my values and every inline image carrying my data attributes, then groups it by asset:
- thumbnail and title, so it reads as pictures rather than a list of IDs;
- every item using it, with path, item ID and a direct edit link;
- link health, from a server-side check of the stored URL;
- expiry status from the stored metadata, as a warning rather than a block.
Three implementation notes. The walk is cached for a minute and returns a "truncated" flag rather than falling over on a big tree. Writes through the same client are limited to paths under the content root and to fields that are genuinely mine, so even if that endpoint were abused it could only touch my own fields. And link checks run in the background, because a slow CDN should make the report slower, not broken.
17Six limits you should know about
Each of these cost me at least a day to establish. None is a bug. They are consequences of running on a SaaS platform, which on the whole is a trade I am happy with. An AI agent will not warn you about any of them, because they are not in the documentation. Design around them and you will move faster than I did.
| The limit | What to do instead |
|---|---|
| No way to add a rich text toolbar button no extension point, and no custom server code allowed to register one |
Ship your own rich text field as a custom field. |
| Nothing renders in the classic Content Editor these fields show raw JSON there; Bynder's connector has exactly the same limitation |
Point authors at the Pages content tab. I also built a standalone field editor over the Authoring API for people who work that way. |
| The field Source cannot carry settings | Put them in standard values and decide what to render from the shape of the value (section 5). |
| The SDK never settles outside a Sitecore host open the app directly and it neither succeeds nor fails, it just waits forever |
Race it against your own timeout, and treat the timeout as a switch into standalone mode rather than an error screen. |
| Components are not addable until the website knows them | Treat every connector component as two deployments that have to land together (section 5). |
| Your app's sign-in client may not do server-to-server the Marketplace app client is interactive only |
Get a separate automation client provisioned early, before the sprint that needs it. |
18Build this for your own DAM in a week
Here is the part I did not expect when I started. Almost none of this is specific to our DAM.
Look at what the connector is made of. A picker built from wireframes. A data format. A server layer that proxies an API and holds the secrets. An adapter that writes to a Sitecore field. Base templates. A package that renders on the website. A usage report. Tests around all of it. Exactly one piece knows anything about Brandworkz: the module that talks to the DAM.
First, check your DAM can support it
Before you plan anything, confirm the API gives you these five things. If it does, everything in this article applies. If the last one is missing, stop and talk to your vendor before writing code, because it is the one thing you cannot work around.
| You need | Why |
|---|---|
| Server-side authentication | A machine credential your backend can use. If the only way in is an interactive browser login, the whole proxy model gets much harder. |
| Search with paging | The picker is mostly search. You also want a type filter, or you will have to filter client-side and the counts will lie. |
| A folder or category listing | Authors browse as well as search, and locking a field to a folder is one of the most requested settings. |
| Asset detail by ID | For the preview, the metadata, and refreshing a stored reference later. Check the ID you get from search is the same one detail accepts. |
| A permanent public URL | The whole model rests on this. It must be stable, public, and ideally available per size. Confirm it is covered by your licence, not just technically possible. |
What you reuse, and what you rewrite
Stays the same
The architecture. The wireframes. The design system choice. The field format. The picker. The host interface. The save-and-wait discipline. The Sitecore field type, the Source rule, the settings-in-standard-values transport, the base templates. The renderers. The usage report. The tests. The whole AI workflow.
Changes per DAM
One module: how you authenticate, search, list folders, fetch asset detail, and get a public URL. Plus the mapper that turns their response into your data format, and the provider value in the JSON.
The week
This is the plan I would follow, knowing what I know now. It assumes one engineer with an AI agent, credentials in hand, and Cloud Portal access already sorted.
Day 0, half a day — set the rules
Create the repo with the folder discipline from section 7. Write the rules file. Start the decision log. Register the Marketplace app and note the App ID. Get the DAM credentials into environment variables. No product code today.
Day 1 — probe the API, then write the format
Write the probe script and run it against the live tenant. Record the auth mechanism, the response shape, paging, ID casing, and which fields actually come back. Then write your field format and get it reviewed by whoever owns content migration. Both artefacts land in the knowledge folder.
Day 2 — the server layer
The DAM client, the shared route wrapper, and the search, folders, detail and health routes. Mappers that turn their responses into your DTOs, with everything optional. Unit tests for the format and the mapping. By the end of the day the smoke script should return real assets through your own API.
Day 3 — the picker, standalone
Search, folder tree, results grid, detail view, alt text gate, on top of the host interface with the clipboard implementation. Keyboard support and accessibility checks as you go. Demo it to someone who will use it, on its own URL, with no Sitecore involved.
Day 4 — inside Sitecore
Point the extension points at your deployment. Add the Sitecore host implementation with the save, wait, close discipline. Create one plugin field by hand and prove the round trip: open, save, close, reopen, read it back. Then script the base templates so nobody has to repeat that by hand. This is the highest-risk day, which is why it comes before the polish.
Day 5 — the website half and the handover
The renderer package and its parser. One browser test for the main journey. The admin screen with a connection test. A short deploy runbook. Whatever you learned this week, written down properly, because that file is what makes week two fast.
What will actually slow you down
Not the code. In my experience it is these four, so start them in parallel on day zero:
- Access. Cloud Portal permissions, an App ID, a DAM service account, and an automation client for server-to-server calls. Every one of these needs somebody else to press a button.
- Public URL licensing. Confirm in writing that your DAM licence covers permanent public delivery links. This is the single question most likely to stop the project.
- Vendor auth surprises. Budget a day for whatever your DAM does that the documentation does not mention. There will be something.
- The website deployment. If you ship components, you depend on the site team's release cycle. Ask early.
The AI workflow is what makes a week realistic rather than optimistic. The expensive part of a connector was never the idea, it was the volume of careful, boring work: screens, states, mappers, validation, tests, templates, documentation. That is exactly the work an agent is good at when you give it a clear architecture, deterministic checks, and a memory of what it has already learned. The part that stays human is the judgement in sections 3 to 5. That did not get automated, and I do not think it is about to be.
If you remember six things
- Decide the architecture before you write code. Who talks to whom, what gets stored, and which parts you expect to change.
- Write the field format first, and get it signed off by whoever will migrate content into it.
- Design how the fields get created in Sitecore, not just how the picker looks. Base templates are what make it usable.
- Write a live probe script on day one. Replace what the model assumes with what your tenant does.
- Put an interface in front of anything you do not control, and build the standalone version. Both are nearly free early and expensive later.
- Write down every failure the day it happens. That file is what makes the next session faster than the last one.
There is nothing clever in this connector. It is an app, a proxy, a schema, an interface with three implementations, and some React components. The hard part was never the technology. It was deciding the architecture properly, refusing the shortcuts, and keeping a process tight enough that moving fast did not mean moving carelessly.
About this write-up
Everything here comes from a working connector: a custom, organisation-scoped Marketplace app that puts a commercial DAM inside XM Cloud Pages. Picker, rich text mode, standalone browsing, base templates, a renderer package for the website, an admin screen and a usage report. Tenant names, IDs and internal identifiers have been changed. The architecture, the traps and the decisions are exactly as they happened.
If you are building something similar, or you have hit a limit I have not listed, I would like to hear about it. The Marketplace extension model is new enough that our shared notes are still worth more than the documentation.