Chain apps: from an email to an issue to a message

Run a job across several apps: read a Gmail thread, file a Linear issue about it and post the link in Slack, with the agent carrying ids between steps and checking before it creates anything.

This recipe runs one job across three apps: read a customer's Gmail thread, file a Linear issue about it, then post the issue link in a Slack channel. It works the same for any apps you connect.

Status: Shipping

How chaining works

There is no workflow engine on the Flux side. Your agent's own model does the chaining:

  • It makes one apps_execute call per step. Each call runs exactly one action.
  • It reads each step's result and carries what the next step needs (a thread id, an issue link) into that step's arguments.
  • Nothing is rolled back. If the Slack post fails, the Linear issue still exists. So the agent should check before it creates anything, and retry only the step that failed.

Connected apps are shared by every key on your Flux account. Any key that can call the tools server can run this job in these apps. See Who can see your connections.

Before you start

Know the Gmail thread you mean, the Linear team to file in and the Slack channel to post to. Then add the Flux tools server to your agent, if you have not already:

Built into Murage, which decides which abilities it uses. Nothing to set up here. See murage.ai.

Step 1: connect the three apps

Ask your agent:

Using the Flux apps tools, connect Gmail, Linear and Slack. Give me each sign-in link, then check apps_list until all three show as connected.

The agent calls apps_connect three times. You open each link and sign in. When apps_list shows "connected": true for gmail, linear and slack, you are ready. An app you have connected before is skipped.

Step 2: run the job

Give the whole job in one message, including the check:

Using the Flux apps tools:

  1. Find the Gmail thread from Acme about the export bug and read it. Note the thread id.
  2. Search Linear for an open issue that mentions that thread id. If there is one, use it and do not create another.
  3. If there is none, create a Linear issue in the Support team. Title it from the thread's subject, summarise the problem in the description, and put gmail-thread: <thread id> on the last line.
  4. Post the issue's link in the #support Slack channel, with a one-line summary. Use apps_find_actions to find each action before you run it. If a step fails, stop and tell me which one; do not start again from the top.

What the agent does with that:

StepTool callWhat it carries forward
Find the actionsapps_find_actions for each task (not billed)The action slugs and their argument names
Read the threadapps_execute with the Gmail actionThread id, subject, the customer's words
Check for an issueapps_execute with the Linear search actionAn existing issue link, or nothing
Create the issueapps_execute with the Linear create action, only if nothing was foundThe new issue's id and link
Post to Slackapps_execute with the Slack message actionNothing: the job is done

Why the check matters

The thread id on the last line of the issue is a marker. It makes the job safe to run twice: on a second run, step 2 finds the issue and step 3 is skipped, so a retry or a repeated request never files a duplicate.

Use the same pattern whenever a chain creates something: put a stable id from the source into what you create, and search for that id before you create it again.

If a step fails

What went wrongWhat to do
A step answers "ok": falseThe app refused the action. Read its reason in result, fix the arguments, and run that step again with the values already carried.
rate_limitedWait for retry_after_seconds, then run the same step again.
payment_requiredThe free runs are used up. Add credit in Billing, then carry on from the step that failed.
invalid_request on an actionThe slug or an argument name was wrong. Ask apps_find_actions again.
The agent created two issuesIt skipped the check. Add the marker and the search step to your prompt, as above.

More fixes are on App problems.

From your own code

The same job in code. The script calls the tools server directly, one step at a time, and stops at the first failure. Take each action slug and its argument names from apps_find_actions; the ones below are examples.

python
import json
import requests

TOOLS_URL = "https://api.fluxrouter.ai/mcp-tools/"
HEADERS = {
    "Authorization": "Bearer sk-YOUR-FLUX-KEY",
    "Content-Type": "application/json",
    "Accept": "application/json, text/event-stream",
}


def call_tool(name, arguments=None):
    """Call one Flux tool and return its structured answer."""
    body = {"jsonrpc": "2.0", "id": 1, "method": "tools/call",
            "params": {"name": name, "arguments": arguments or {}}}
    res = requests.post(TOOLS_URL, headers=HEADERS, json=body, timeout=320)
    res.raise_for_status()
    for line in res.text.splitlines():
        if line.startswith("data:"):
            result = json.loads(line[len("data:"):])["result"]["structuredContent"]
            if "error" in result:
                raise RuntimeError(f"{name}: {result['error']}: {result.get('message')}")
            return result
    raise RuntimeError(f"{name}: no answer")


def run(action, arguments):
    """One step: run one action, and stop the job if the app refused it."""
    out = call_tool("apps_execute", {"action": action, "arguments": arguments})
    if not out["ok"]:
        raise RuntimeError(f"{action} failed: {out['result']}")
    return out["result"]


# See what each app offers for these tasks (not billed).
for task, app in [("fetch an email thread", "gmail"),
                  ("search issues", "linear"),
                  ("create an issue", "linear"),
                  ("send a message to a channel", "slack")]:
    print(app, json.dumps(call_tool("apps_find_actions", {"task": task, "app": app})["actions"])[:500])

# 1. Read the thread.
thread_id = "18f2a9c4e1b7d305"
thread = run("GMAIL_FETCH_MESSAGE_BY_THREAD_ID", {"thread_id": thread_id})
marker = f"gmail-thread: {thread_id}"

# 2. Look before creating: is there already an issue with this marker?
found = run("LINEAR_SEARCH_ISSUES", {"query": marker})
issues = found.get("issues") or []

# 3. Create the issue only if none exists.
if issues:
    issue_url = issues[0]["url"]
else:
    created = run("LINEAR_CREATE_ISSUE", {
        "team_id": "SUPPORT_TEAM_ID",
        # Build the title and summary from what `thread` returned.
        "title": "Export bug reported by Acme",
        "description": f"Summary of the customer's report.\n\n{marker}",
    })
    issue_url = created["url"]

# 4. Tell the team.
run("SLACK_SEND_MESSAGE", {"channel": "#support", "text": f"New support issue: {issue_url}"})

Each app's result is its own shape, so read the fields your actions return (for example the issue's link) from the apps_find_actions answer and a first test run.

What it costs

  • apps_find_actions, apps_list and apps_connect are not billed. On an account without a paid plan or credit, each apps_connect uses one free run.
  • Each apps_execute is one action run. This job is four runs, or three when the issue already exists.
  • Your agent's own thinking between steps is billed as normal chat on the same key.
  • App actions: priced per call.

Prices and limits are on Use apps.

Another chain: a weekly digest

The same pattern works for reading several things and writing one. For example:

Using the Flux apps tools, list the pull requests merged in our GitHub repo acme/web in the last week, then create a Notion page in the Engineering space titled with this week's date, listing each one with its link and a one-line summary.

Here the agent reads first (one or more GitHub runs), then writes once (one Notion run). There is nothing to check for duplicates unless you run it twice in a week; if you might, ask the agent to search Notion for a page with that title first.