LangChain and LangGraph Tutorial

Let LangChain post to X
with one Python tool

Give your LangChain or LangGraph agent the ability to publish, schedule, and thread on X. One @tool that calls the OpenTweet REST API, or the hosted MCP for all 36 tools.

7-day free trial • No credit card required

A tweet tool your agent can call

LangChain agents act through tools. To let one post to X, you give it a function that sends a POST request to OpenTweet's REST API athttps://opentweet.io/api/v1/postswith your API key as a Bearer token. Decorate it with@tool, bind it to the model, and the agent can post whenever it decides to.

Wiring the agent straight to the X API means an approved developer account, OAuth 1.0a consumer keys, and manual request signing on every call. That is a lot of ceremony to bury inside a tool function, and it makes the agent code hard to follow.

OpenTweet replaces all of it with a single ot_ key and JSON payloads. No developer portal application, no OAuth token exchange, and no signing. The tool body stays short enough to read at a glance.

x_api_oauth.py
# The old way (X API + OAuth 1.0a)
import tweepy

# Step 1: approved X developer account
# Step 2: consumer + access token pairs
auth = tweepy.OAuth1UserHandler(
    consumer_key="...",
    consumer_secret="...",
    access_token="...",
    access_token_secret="...",
)
api = tweepy.API(auth)

# Step 3: still no scheduling, no threads,
# and OAuth token plumbing in your tool.
opentweet_tool.py
# The new way (OpenTweet API)
import os, requests
from langchain_core.tools import tool

@tool
def post_tweet(text: str) -> str:
    """Publish a tweet to X right now."""
    requests.post(
        "https://opentweet.io/api/v1/posts",
        headers={"Authorization": f"Bearer {os.environ['OPENTWEET_API_KEY']}"},
        json={"text": text, "publish_now": True},
    )
    return "posted"
# Done. No OAuth. No signing.

Step-by-Step: LangChain to X

1

Get an ot_ API key

Sign up for OpenTweet, connect your X account, and open the developer page at opentweet.io/developer. Generate an API key. It starts with ot_ and is the only credential the agent needs. Store it in an environment variable, never in your source.

bash
# Store your key as an environment variable
export OPENTWEET_API_KEY="ot_your_key"

# Sanity check the key with GET /me
curl https://opentweet.io/api/v1/me \
  -H "Authorization: Bearer $OPENTWEET_API_KEY"
2

Define a post_tweet LangChain tool

Wrap a requests.post to /api/v1/posts in a function decorated with @tool. The docstring becomes the tool description the model reads, so keep it clear. Set publish_now to True to send it live.

tools.py
import os
import requests
from langchain_core.tools import tool

BASE = "https://opentweet.io/api/v1"

@tool
def post_tweet(text: str) -> str:
    """Publish a tweet to X immediately. Pass the full tweet text."""
    res = requests.post(
        f"{BASE}/posts",
        headers={
            "Authorization": f"Bearer {os.environ['OPENTWEET_API_KEY']}",
            "Content-Type": "application/json",
        },
        json={"text": text, "publish_now": True},
    )
    if res.status_code == 401:
        return "Error: bad or missing ot_ key"
    if res.status_code == 403:
        return "Error: your plan or account cannot post"
    if res.status_code == 429:
        return "Error: rate limited, back off and retry"
    if not res.ok:
        return f"Error {res.status_code}: {res.json().get('error', '')}"
    return f"Posted. id: {res.json().get('id')}"

The docstring is the tool description

LangChain feeds the function docstring and type hints to the model as the tool schema. Write it the way you would explain the tool to a teammate so the agent calls it correctly.
3

Register the tool with your agent

Bind post_tweet to your model, or pass it in the tools list when you build a LangGraph ReAct agent with create_react_agent. The agent now decides when to call it.

agent.py
from langchain.chat_models import init_chat_model
from langgraph.prebuilt import create_react_agent
from tools import post_tweet

model = init_chat_model("anthropic:claude-sonnet-4-5")

agent = create_react_agent(model, tools=[post_tweet])

result = agent.invoke({
    "messages": [
        {"role": "user", "content": "Write a short tweet about shipping fast and post it."}
    ]
})
print(result["messages"][-1].content)

Plain LangChain works too

Not on LangGraph? Call model.bind_tools([post_tweet]) and run the tool-calling loop yourself. The same tool function powers both.
4

Add scheduling and threads

Give the agent more reach with two more tools. One schedules a tweet by sending a scheduled_date in ISO 8601 instead of publish_now. The other posts a native thread with is_thread and thread_tweets.

more_tools.py
@tool
def schedule_tweet(text: str, scheduled_date: str) -> str:
    """Schedule a tweet. scheduled_date is ISO 8601, e.g. 2026-07-10T09:00:00Z."""
    res = requests.post(
        f"{BASE}/posts",
        headers={
            "Authorization": f"Bearer {os.environ['OPENTWEET_API_KEY']}",
            "Content-Type": "application/json",
        },
        json={"text": text, "scheduled_date": scheduled_date},
    )
    return "scheduled" if res.ok else f"Error {res.status_code}"

@tool
def post_thread(tweets: list[str]) -> str:
    """Post a native X thread. Pass the tweets in order."""
    res = requests.post(
        f"{BASE}/posts",
        headers={
            "Authorization": f"Bearer {os.environ['OPENTWEET_API_KEY']}",
            "Content-Type": "application/json",
        },
        json={
            "is_thread": True,
            "thread_tweets": tweets,
            "thread_media": [[] for _ in tweets],
            "publish_now": True,
        },
    )
    return "posted thread" if res.ok else f"Error {res.status_code}"

publish_now and scheduled_date are exclusive

Send one or the other, never both in the same request. Leave both out and OpenTweet saves the post as a draft the agent can schedule later.
5

Or point the agent at the hosted MCP

Rather than hand-write each tool, connect your agent to the hosted MCP server at https://mcp.opentweet.io/mcp over streamable-http with your ot_ key as a Bearer header. That exposes all 36 OpenTweet tools at once, so the agent can post, schedule, thread, batch, and pull analytics without you writing a single request.

mcp_agent.py
import os
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.prebuilt import create_react_agent
from langchain.chat_models import init_chat_model

client = MultiServerMCPClient({
    "opentweet": {
        "transport": "streamable_http",
        "url": "https://mcp.opentweet.io/mcp",
        "headers": {
            "Authorization": f"Bearer {os.environ['OPENTWEET_API_KEY']}",
        },
    }
})

tools = await client.get_tools()  # all 36 OpenTweet tools
agent = create_react_agent(init_chat_model("anthropic:claude-sonnet-4-5"), tools)

await agent.ainvoke({
    "messages": [{"role": "user", "content": "Schedule a tweet for tomorrow 9am UTC about our launch."}]
})

Prefer a local MCP process?

You can also run the server locally with the command npx and args -y @opentweet/mcp-server, passing OPENTWEET_API_KEY in the environment. See the full tools reference.

Pro Tips

Return strings the model can read

A LangChain tool should return a short status string, not raise. When you turn a 401, 403, or 429 into "Error: rate limited", the agent can react and retry instead of crashing the run.

Keep the ot_ key in the environment

Read os.environ["OPENTWEET_API_KEY"] inside the tool. Add it to a local .env and to your host or CI secrets for production. Never bake the key into the agent prompt.

Start with one tool, grow to MCP

A single post_tweet tool is enough to prove the loop. When the agent needs threads, batch scheduling, or analytics, switch to the hosted MCP and get all 36 tools without more code.

Let the model draft, the tool ship

Have your agent generate the copy in one node, then call post_tweet to publish. That splits writing from shipping and keeps the tool boundary clean.

Common Mistakes to Avoid

Retrying permanent 4xx errors

A 400, 401, or 403 will never succeed on retry. Fix the request or the key instead. Only back off and retry on 429 and 5xx, and use exponential backoff when you do.

Sending publish_now and scheduled_date together

They are mutually exclusive. Send one or the other. Include both and the request is rejected. Omit both only when you want a draft the agent schedules later.

Using api.opentweet.io

That host does not resolve. The REST base is https://opentweet.io/api/v1. Point every tool there and keep your ot_ key in the Authorization header.

Frequently Asked Questions

How do I let a LangChain agent post to Twitter?

Define a function that sends a POST request to https://opentweet.io/api/v1/posts with your ot_ key as a Bearer token, decorate it with LangChain's @tool, and bind it to your model. The agent can then call the tool with tweet text and OpenTweet publishes to X. No X developer account and no OAuth 1.0a signing.

Does this work with LangGraph too?

Yes. The same @tool works in LangGraph. Add post_tweet to the tools list when you build a ReAct agent with create_react_agent, or bind it to the model in a custom graph node. LangGraph handles the tool-calling loop and the agent decides when to post.

Should I use a single tool or the hosted MCP?

Use a single post_tweet tool when you only need to publish or schedule. Point the agent at the hosted MCP server at https://mcp.opentweet.io/mcp when you want the full surface, since it exposes all 36 OpenTweet tools including threads, batch scheduling, analytics, and evergreen. Both authenticate with the same ot_ Bearer key.

Do I need the Twitter API or OAuth for this?

No. OpenTweet uses one ot_ Bearer key and plain JSON, so there is no X developer account, no OAuth 1.0a request signing, and no callback URLs. Your LangChain tool just calls a REST endpoint, which keeps the agent code short and easy to reason about.

Give your agent an X tool today

Get your ot_ key in 2 minutes. Add one @tool and let LangChain post to X.