Jules Turndown
October 29, 2026
We’re writing to let you know that as part of our work to unify our agentic coding efforts, Jules will wind down on December 31, 2026. You have until November 11, 2026 to continue finishing up sessions. After November 11, 2026, we will be turning Jules to read-only until the end of the year.
When we launched Jules, it was an experiment to learn how asynchronous coding agents could help you tackle your backlog and ship faster. What you built with it taught us where agentic development is headed, and those learnings are shaping products across Google, including our agent-first platform, Google Antigravity.
You can now export everything Jules knows about your work into a local folder: account settings, environment variables, scheduled tasks, connected repositories, memories, setup scripts, and every session, including its prompt, plan, full activity log, and code patches.
Your code, branches, and pull requests already live in GitHub, so they stay where they are. The export covers the configuration and history that live inside Jules.
To export, install Jules Tools and sign in:
npm install -g @google/julesjules loginThen open a CLI coding agent, such as Claude Code, from your home directory and paste the export prompt below. The agent checks your setup, runs the export, and tells you what it saved.
Show the export prompt
I want to export all my data from Google Jules into a local folder: settings, environmentvariables, scheduled tasks, connected repos, memories, setup scripts, and every session (prompt,plan, full activity log, and code patches). Please do this in order:
1. Setup - Check whether the Jules CLI is installed (`which jules`). If it isn't, install it with `npm install -g @google/jules`. If npm isn't installed, tell me and stop. - Check whether I'm logged in: `security find-generic-password -s jules-cli -a default` on macOS (metadata only, without -w), or `secret-tool lookup service jules-cli username default` on Linux. If there's no entry, ask me to run `! jules login` myself. It's an interactive browser sign-in, so don't try to run it for me. Wait until I say it's done. - Run `jules remote list --repo`. If it fails with a 401 and asks about GitHub, tell me to connect my GitHub account at https://github.com/apps/google-labs-jules/installations/select_target and wait for me to confirm. If I've never used Jules with GitHub repos, I can skip this; the export will just have no repo data.
2. Export: save the script below to a file and run it from my home directory. Then tell me what was exported (the counts it prints) and the full path of the folder. Don't print my access token or the values in env-variables.json.
Problems someone else already ran into:- The Jules CLI stores its login in the macOS Keychain (service "jules-cli", account "default") as "go-keyring-base64:<base64 JSON>". The script decodes that. The access token expires about an hour after login; if you get 401s on every call, run any `jules` command or ask me to run `! jules login` again.- If ListSources returns 401 "missing required authentication credential" but the other calls work, my GitHub account isn't connected to Jules (see step 1).- Settings, env vars, repos and memories come from an internal `$rpc` endpoint. Sessions come from REST endpoints under https://aida.googleapis.com/v1/swebot (/tasks, /tasks/{id}, /tasks/{id}/activities), which are what the CLI itself uses. Both are undocumented, so if a call starts returning 404/400, tell me which one rather than guessing at fixes.- The repo list doesn't include setup scripts or test commands; the script recovers them from each session's code output (repoMetadata). Empty memories responses ({}) just mean there are no memories.- The API may return a few more sessions than `jules remote list --session` shows (older ones). That's expected.
```pythonimport base64, json, pathlib, re, subprocess, sys, urllib.error, urllib.parse, urllib.request
def get_token(): for cmd in [["security", "find-generic-password", "-s", "jules-cli", "-a", "default", "-w"], ["secret-tool", "lookup", "service", "jules-cli", "username", "default"]]: try: out = subprocess.check_output(cmd, stderr=subprocess.DEVNULL).decode().strip() if out.startswith("go-keyring-base64:"): out = base64.b64decode(out.split(":", 1)[1]).decode() if out: return json.loads(out)["access_token"] except Exception: pass p = pathlib.Path.home() / ".jules/cache/oauth_creds.json" if p.exists(): return json.loads(p.read_text())["access_token"] sys.exit("Run `jules login` first.")
TOKEN = get_token()HEADERS = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}RPC = "https://aida.googleapis.com/$rpc/language_labs.aida.swebot.proto.SweBotService"REST = "https://aida.googleapis.com/v1/swebot"OUT = pathlib.Path("./jules-export"); OUT.mkdir(exist_ok=True)errors = []
def rpc(m, b=None): req = urllib.request.Request(f"{RPC}/{m}", data=json.dumps(b or {}).encode(), headers=HEADERS, method="POST") with urllib.request.urlopen(req, timeout=60) as r: return json.loads(r.read().decode() or "{}")
def get(path, **params): q = ("?" + urllib.parse.urlencode(params)) if params else "" with urllib.request.urlopen(urllib.request.Request(f"{REST}{path}{q}", headers=HEADERS), timeout=60) as r: return json.loads(r.read().decode() or "{}")
def paged(fetch, key): items, token = [], None while True: resp = fetch(token) items += resp.get(key, []) token = resp.get("nextPageToken") if not token: return items
def safe(s): return re.sub(r"[^a-zA-Z0-9._-]", "_", s)def dump(path, obj): path.parent.mkdir(parents=True, exist_ok=True); path.write_text(json.dumps(obj, indent=2))
# Account-level datadump(OUT / "user-settings.json", rpc("GetUserSettings"))dump(OUT / "env-variables.json", rpc("ListEnvironmentVariables"))dump(OUT / "scheduled-tasks.json", rpc("ListScheduledTasks"))
# Repos (sources) and their memoriessources = paged(lambda t: rpc("ListSources", {"pageSize": 100, **({"pageToken": t} if t else {})}), "sources")dump(OUT / "sources.json", sources)mem_repos = 0for src in sources: sid = src.get("sourceId") or src.get("id") or src.get("name", "").removeprefix("sources/") try: mems = paged(lambda t: rpc("ListMemories", {"sourceId": sid, "pageSize": 100, **({"pageToken": t} if t else {})}), "memories") except urllib.error.HTTPError as e: errors.append(f"memories {sid}: HTTP {e.code}"); continue if mems: mem_repos += 1 d = OUT / "repos" / safe(sid) dump(d / "memories.json", mems) (d / "memories.md").write_text("# Memories\n\n" + "\n".join( f"- {m.get('Fact') or m.get('fact') or m.get('content')}" for m in mems if not m.get("archived")) + "\n")
# Sessions: full detail, activity log, patches, and a readable summarysessions = paged(lambda t: get("/tasks", pageSize=100, **({"pageToken": t} if t else {})), "tasks")dump(OUT / "sessions.json", sessions)setup = {} # latest setup script / test command seen per repopatches = 0for s in sorted(sessions, key=lambda s: s.get("createdAt", "")): tid = s["id"]; d = OUT / "sessions" / tid try: task = get(f"/tasks/{tid}").get("task", {}) acts = paged(lambda t: get(f"/tasks/{tid}/activities", pageSize=100, **({"pageToken": t} if t else {})), "activitySteps") except urllib.error.HTTPError as e: errors.append(f"session {tid}: HTTP {e.code}"); continue dump(d / "session.json", task) dump(d / "activities.json", acts) commits = [o["gitCommit"] for o in task.get("outputs", []) if "gitCommit" in o] for i, c in enumerate(commits, 1): if c.get("patch", {}).get("unidiff"): (d / f"patch-{i}.diff").write_text(c["patch"]["unidiff"]); patches += 1 meta = c.get("repoMetadata", {}) if meta.get("environmentSetupScript") or meta.get("testCommand"): setup[task.get("sourceId") or meta.get("name") or tid] = meta plan = task.get("latestPlan", {}).get("steps", []) kinds = {} for a in acts: for actor, v in a.items(): if isinstance(v, dict): for k in v: kinds[f"{actor}.{k}"] = kinds.get(f"{actor}.{k}", 0) + 1 lines = [f"# {task.get('title') or s.get('suggestedTitle') or tid}", "", f"- **ID:** {tid}", f"- **Repo:** {task.get('sourceId', '')}", f"- **Status:** {task.get('taskStatus', '')}", f"- **Created:** {task.get('createdAt', '')}", f"- **Completed:** {task.get('completedAt', '')}", "", "## Prompt", "", task.get("description", ""), ""] if plan: lines += ["## Latest plan", ""] + [f"{p.get('index', 0) + 1}. {p.get('title', '')}" for p in plan] + [""] if commits: lines += ["## Code outputs", ""] + [ f"- `patch-{i}.diff`: {c.get('title') or (c.get('commitMessage') or '').split(chr(10))[0]} (branch `{c.get('gitBranchName', '')}`)" for i, c in enumerate(commits, 1)] + [""] if pub := task.get("latestPublish", {}).get("commitInfo"): lines += ["## Published", "", f"- Commit `{pub.get('commitId', '')}` on `{pub.get('branchName', '')}`", ""] lines += ["## Activity", "", f"{len(acts)} steps (full log in `activities.json`):", ""] + [f"- {k}: {n}" for k, n in sorted(kinds.items())] (d / "summary.md").write_text("\n".join(lines) + "\n")
for sid, meta in setup.items(): d = OUT / "repos" / safe(sid); d.mkdir(parents=True, exist_ok=True) if meta.get("environmentSetupScript"): (d / "setup.sh").write_text(meta["environmentSetupScript"] + "\n") if meta.get("testCommand"): (d / "test-command.txt").write_text(meta["testCommand"] + "\n")
print(json.dumps({"repos": len(sources), "repos_with_memories": mem_repos, "sessions": len(sessions), "patches": patches, "repos_with_setup_or_test": len(setup), "error_count": len(errors), "errors": errors[:10]}, indent=2))print(f"Exported all Jules data to {OUT.resolve()}")```Your data lands in ~/jules-export: connected repositories in sources.json, a folder per repository with its setup script, test command, and memories, your settings, environment variables, and scheduled tasks as JSON, and a folder per session with a readable summary.md, the full activity log, and its diffs.
env-variables.json contains your environment variable values, so keep the export folder private and out of your repositories. If you use Jules with GitHub, make sure your GitHub account is connected first, or the export won’t include repository data.