LangChain Messages : Content vs ContentBlocks properties

In the LangCainJS documentation it is stated

Content blocks were introduced as a new property on messages in LangChain v1 to standardize content formats across providers while maintaining backward compatibility with existing code.

Content blocks are not a replacement for the content property, but rather a new property that can be used to access the content of a message in a standardized format.

I’m a bit confused by the phrasing here.

  1. Content blocks are not a replacement for the content property

When invoking a model with a HumanMessage, we can construct a HumanMessage with these contentBlocks.

Should we continue using the content property, or switch to the new contentBlocks property to define what we want to send ?

I believe all messages below are equivalent but content blocks offer a more standardized and type safe way of defining the input.

Or is the first option (using the standard langchain content format) sufficiently “standardized” to work across all providers already ?

// 1. Standard lanchain content format ?
new HumanMessage({
    content: [
        { type: "text", text: "Describe the content of this document." },
        { type: "image", source_type: "url", url: publicImageUrl, },
    ],
});

// 2. openai native content format ?
new HumanMessage({
        content: [
            { type: "text", text: "Describe this image." },
            {
                type: "image_url",
                image_url: { url: publicImageUrl },
            },
        ],
    });

// 3. new contentBlock typed api
new HumanMessage({
        contentBlocks: [
            {type: "text", text: "Describe this image."},
            {type: "image", url: publicImageUrl},
        ],
    });


Is using the new contentBlock types the way to go also when constructing messages (and if so would they not act as a replacement for the content property ?

  1. lazily parse the content attribute into a standard, type-safe representation.

Another use-case as shown in the docs seems to be to “lazy-load” the existing content property into the new contentBlocks typing.

Use-case here is having a type safe and unified way to parse model results (regardless of providers I assume ?) (at the time of writing not all providers have support for these contentBlocks so not all of them can be lazy loaded into the new typing.

Is my understanding correct here ?

Should we continue using the content property, or switch to the new contentBlocks property to define what we want to send ?

IMO yes! Once you get over the initial hurdle of how our content standard works, you can take a lot of the obscure cases that come with moving message objects between providers out of the equation.

(and if so would they not act as a replacement for the content property ?

We can’t represent every possible permutation of content that a provider will accept through a standard message abstraction (in other words, we couldn’t have a standard set of content types without adding some really obscure properties). We thought it would still be important to let you express those behaviors inside of content, but in order to also have a usable standard (and not break a bunch of existing user code) it had to be treated as separate.

Or is the first option (using the standard langchain content format) sufficiently “standardized” to work across all providers already ?

It is, but there are some notable feature gaps within the existing content format that we don’t standardize on across all providers. contentBlocks is meant to address that

(at the time of writing not all providers have support for these contentBlocks so not all of them can be lazy loaded into the new typing.

Important thing to clarify – all providers can be parsed into this standard contentBlocks, but when we say “it doesn’t have support” it more has to do with us not having verified that we’re representing every aspect of X provider’s message object as content blocks. If we don’t have a dedicated parser, we do a “best-effort” parse to get important content out (text + reasoning IIRC). Worst case is you drop a minimal amount of context when going from provider to provider

@hntrl Not sure if I miss something, but the docs mention a few different options to create a multimodal human message, and none of those work. Messages - Docs by LangChain

E.g.

const humanMessage = new HumanMessage({
  contentBlocks: [
    { type: "text", text: "Hello, how are you?" },
    { type: "image", url: "https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png" },
  ],
});

Doesn’t work. The LLM will ignore the image.

If I add source_type=url, then the LLM will complain that the mimeType is missing.

If I add the mimeType=image/png parameter, nothing changes. Same error.

Finally, when I add a mime_type=image/png, it works.

But now we’re not even conforming to the new ContentBlock specification anymore. I’m a bit lost to be honest… What is the standard way of doing things? Is the new ContentBlock type even supported on input messages?

Hey @ddewaele ,

You’re not missing something — what you’re running into is the difference between:

  1. LangChain’s internal contentBlocks standard, and

  2. What individual providers actually require at input time.


Short Answer

Yes, contentBlocks is supported on input messages.

But:

  • Not all providers fully support the normalized ContentBlock format on input

  • Some providers still require provider-specific fields like:

    • source_type

    • mime_type (snake_case, not camelCase)

    • mimeType depending on the SDK layer

So when it only works after adding mime_type=image/png, that’s because you’re satisfying the provider’s native schema — not just LangChain’s abstraction.


What’s Going On

There are two layers:

:one: Parsing layer (output → standard format)

All providers can be parsed into contentBlocks on output.
That part is standardized.

:two: Input layer (your message → provider)

This is where things are less unified.

Some providers:

  • Fully accept standard contentBlocks

  • Others require extra fields to properly interpret images

  • Some need mime_type

  • Some expect image_url structure instead

If there isn’t a dedicated parser/serializer for that provider yet, LangChain falls back to a “best effort” conversion.

That’s why:

  • Your first version silently ignored the image

  • The second version errored

  • The third version worked only after adding provider-specific fields


Current Reality (Feb 2026)

  • contentBlocks is the future direction

  • It is safe and recommended

  • But you may still need provider-specific fields depending on which model you’re calling

So yes — the new ContentBlock type is supported, but provider compatibility is still catching up in some cases.


Practical Recommendation

If you’re working with multimodal input today:

  • Use contentBlocks

  • Add mime_type when sending images

  • Confirm against the specific provider’s multimodal docs

If you share which provider/model you’re using, we can give a precise example.


You’re absolutely right that the docs could make this clearer — the current state is “standardized abstraction with evolving provider support.”

Thanks for the clarification!

We’re mainly using Google Gemini (and sometimes Azure OpenAI). For Gemini, I’ve tried @langchain/google-vertexai-web, @langchain/google-genai and @langchain/google and they all didn’t work yet. But also not sure what the state/direction of these packages is.

… state/direction of these packages is.

These packages have unfortunately accrued a lot of tech debt, and we generally regard the existing langchain google stack to be pretty complicated. We’ve since launched a new @langchain/google package that has much better support for standard content since we were able to think about it from the jump of designing this new package: https://x.com/LangChain_JS/status/2022016395962400988

Thanks! Then I’ll await the full ContentBlocks support in @langchain/google and use a polyfill for now. Is it correct that the new package does not support Embedding models yet?

Is it correct that the new package does not support Embedding models yet?

Yeah thats correct, what exactly are you trying to do with google embeddings?

We built our RAG data pipelines on top of langchain, so we use the models to create embeddings for our documents and store them in QDrant. But that’s uncommon, maybe? We built our complete AI automation platform on Langchain :wink: https://dataspeak.nl