Claude Code Just Got 4 Huge Upgrades — Here’s What Each One Actually Does

HTML eval report showing per-run scores and pass/fail breakdown

I’ve been using Claude Code for a while now, and most updates are incremental — a quality-of-life tweak here, a small model improvement there. But the v2.1.71 drop (March 2026) genuinely changed how I think about what Claude Code is.

Four features landed at once. Together they shift Claude Code from “chat that helps me code” to something closer to an autonomous system that keeps working while I’m not. Let me walk through each one so you know exactly when and why to use it.

🧠 Feature 1: /loop — Recurring Prompts Inside Your Session

Before this, if you wanted Claude to check something repeatedly, you had to keep coming back, typing the same prompt, waiting, coming back again. Tedious.

/loop changes that. You give it a task in plain English and it sets up a cron job inside your current session that fires automatically.

# Check inbox every 10 minutes — no babysitting needed
/loop every 10 minutes check my inbox for important emails

# Daily content repurposing — runs the skill automatically
/loop every day check my YouTube for new videos and run my content repurposing skill

# One-off reminder (not just recurring)
/loop in 5 minutes remind me to update the PR description

Under the hood, Claude uses three tools: cron_create, cron_list, and cron_delete. Standard 5-field cron expressions with minute-level precision. The natural language gets converted automatically.

sequenceDiagram
    participant Dev as Developer
    participant CC as Claude Code
    participant Cron as Cron Engine

    Dev->>CC: /loop every 10 minutes check my inbox
    CC->>Cron: cron_create("*/10 * * * *", task)
    loop Every 10 minutes
        Cron->>CC: Fire task
        CC->>Dev: Results / notification
    end

Limitations to know going in:

  • Expires after 3 days — intentional safety cap (no runaway loops)
  • Session-only — close the terminal and all loops are gone
  • No catch-up — if a loop fires while the session is closed, it just disappears

📌 Pro Tip: Think of /loop as “help me right now on this project.” Sprint monitoring, inbox watching, checking build status during a deploy — perfect. Long-term automation? That’s next.

🤖 Feature 2: Scheduled Tasks — Permanent Daily/Weekly Automation

/loop is great for bursts. But what about running a skill every Monday morning? Or a daily standup summary at 9 AM? That needs something more permanent.

Scheduled Tasks is that something. You define a task once — with a name, prompt, model, schedule, and folder — and it runs forever. Each run spawns a fresh Claude instance, reads your project files, executes the prompt, and shuts down cleanly.

Here’s a task I’d actually set up:

Name: Repurpose Videos
Prompt: Check my YouTube for new videos and run my content repurposing skill
         to create a newsletter and a tweet thread
Frequency: Daily at 9:00 AM
Model: Claude Sonnet

Claude is smart enough to pull in the right skills automatically based on your prompt wording — it finds the YouTube tool and the content skill without you naming them explicitly.

graph TD
    A[Scheduled Task fires at 9AM] --> B[Fresh Claude instance starts]
    B --> C[Read project files]
    C --> D[Match skills from prompt context]
    D --> E[Execute task]
    E --> F[Save output]
    F --> G[Session ends]

Current limitations:

  • Desktop App only — doesn’t work from the terminal or VS Code extension yet (this will change)
  • Computer must be on and app must be open
  • Does catch up — miss a run? It queues and runs when you reopen. Unlike /loop, nothing is lost.

/loop vs Scheduled Tasks — Quick Reference

/loop Scheduled Task
Scope Current session Persistent
Instance Same throughout Fresh each run
Missed runs Lost forever Catches up
Expiry 3 days No expiry
Where Terminal / VS Code Desktop App only
Best for Right now, this sprint Every day, every week

🛠 Feature 3: Google Workspace CLI — Actually Talk to Your Google Stuff

This one surprised me because it’s not strictly a Claude Code update — but it’s a massive gap filler.

Until now, Claude Code’s built-in skills could manage Gmail and Calendar. But Google Drive, Docs, Sheets, Slides? Nothing. And if you’ve tried building around the Google APIs directly, you know the pain: documents come out with raw markdown, you need extra API calls just to make them look right.

Google released an open-source Workspace CLI that changes the whole equation. One tool. Entire Google ecosystem. 100+ built-in recipes.

# Option A: Install in terminal directly
# (run the install command for the Google Workspace CLI)

# Option B: Just ask Claude Code
"Set up the Google Workspace CLI for me"
# It walks you through the whole thing

The key difference from API-based approaches:

API approach (n8n, etc.):
  Claude → API call → raw markdown → extra formatting calls → "okay" doc

Google CLI approach:
  Claude → bash command → Google → properly formatted doc with headers,
           images, links, tables — done in one step

📌 Pro Tip: Even though Google labels it “not officially supported,” that’s just because it’s in beta. It works, it’s open-source, and the setup is fast. Worth it immediately.

💻 Feature 4: Skills 2.0 — Stop Guessing, Start Measuring

This is the one I’m most excited about. And honestly, the one that’s going to save me the most time.

Here’s the old loop everyone was stuck in:

Build skill → run it → output is okay-ish → tweak something → run again → still not quite right → keep guessing forever

Skills 2.0 breaks that cycle with built-in eval and testing. Anthropic updated their own skill-creator skill to include this. You now get actual scored results against specific criteria — not just “it worked” or “it didn’t.”

How to run a useful eval

The key is being specific. Vague prompt = generic, useless results.

Don’t do this:

Run some tests on my copywriting skill

Do this:

Run a new test optimized for checking whether my copy follows the persuasive
techniques in my persuasion_toolkit.md reference file.

Criteria:
- Does it use the reference file first?
- Does it include curiosity gaps and open loops?
- How often does it use proof or founder-led stories?

Test: Write landing page copy for my school community
Runs: 5 times

Claude launches 5 sub-agents in parallel to run the skill, then returns an interactive HTML report:

HTML eval report showing per-run scores and pass/fail breakdown

The report shows:

  • Per-run output (flick between all 5)
  • Formal grades section with specific failure reasons
  • Benchmark tab: run time and token count per run
  • A/B comparison with and without the skill

Real example output

Curiosity gaps: 50% pass rate (6/12 checks)

Failure reason: The copy lacks genuine curiosity gaps that sustain
across multiple sentences. It teases but immediately reveals.

Action: Strengthen the curiosity gap section in persuasion_toolkit.md
        or add explicit instruction to skill.md

The A/B test workflow

Split terminal, two Claude sessions:

# Session A: with skill
Create landing page copy using the copywriting skill.
Score against persuasion toolkit adherence.

# Session B: without skill
Create landing page copy without any skill.
Score against the same criteria.

This answers: Is my skill actually helping, or just burning tokens?

The iteration loop that actually works

graph LR
    A[Build skill.md] --> B[Run eval: 1-3 criteria]
    B --> C[Review HTML report]
    C --> D[Paste specific feedback to Claude]
    D --> E[Claude edits skill.md]
    E --> B
    B --> F{Score ≥ 9/10?}
    F -->|Yes| G[Ship it]
    F -->|No| D

📌 Pro Tip: Only optimize for 1–3 criteria per eval run. More moving parts = harder to isolate what changed. Get one thing to 9/10, then move to the next.

✅ Why This Set of Updates Matters

These four features aren’t independent tweaks. They stack:

✅ /loop handles the “watch this for me right now” use case
✅ Scheduled Tasks handles the “do this every morning” use case
✅ Google Workspace CLI removes the biggest integration gap
✅ Skills 2.0 turns skill-building from guesswork into engineering

Before these, Claude Code was a very capable assistant you had to keep talking to. Now it’s infrastructure. You set it up, point it at your workflow, and it keeps running.

The shift from “tool I use” to “system that works for me” is a real one — and these four updates are what got it there.

👋 Let’s Connect

If you found this helpful or want to talk about how you’re using Claude Code in your workflow, I’d love to connect!

🔗 Connect with me on LinkedIn
📧 Email: [email protected]

Let’s build something amazing together. 🚀

The Hackers Who Tracked My Sleep Cycle

This is a short story about how I caught hackers timing their attacks around my daily routine.

A couple of weeks ago, in the middle of the night, I got alerts on my phone that various metrics were going haywire.

The only thing I noticed was a huge spike in sign-ups. This coincided with one of my articles being on the front page of Hacker News, so I didn’t think much of it.

It had spiked for a few hours and flattened out just before I started troubleshooting, so I went back to sleep.

When I woke up the next morning, a few thousand more accounts had appeared.

I thought this was a bit suspicious, but … even if someone was being naughty, they seemed to have given up.

Nothing else was out of the ordinary that day. I even decided to stay up later that night to see if the pattern would repeat. Nothing.

As a small precaution, I activated CAPTCHA to slow down the sign-ups and went back to sleep.

The next morning, you guessed it… same pattern.

This time, I decided to do a deep dive into the data.

What I found was that the hackers were…

  1. creating thousands of accounts
  2. adding a valid payment method to each account
  3. running a single very expensive LLM call (2-3 USD)

This would let the first request go through, then trigger a charge to their payment method. The payment method gets rejected, but the request has already been processed.

The reason the first request goes through is that we deposit a little bit of money into the account when it’s created. A nominal amount that’s enough to play with the API. However, if a payment method is added, we allow people to go into overdraft, which is why their expensive LLM call goes through.

Anyway, using this method, they would get away with about a thousand dollars’ worth of credits every night, which kept them interested in the service.

But what caught my attention wasn’t the money – it was the timing. The attacks coincided with my sleep cycle.

At this point, I still thought of it as unlucky timing, but even then something just didn’t sit right with me.

Coincidentally, that day I decided to take a break and disconnect from my computer early. And lo and behold, just 30 minutes after I shut down my computer, I got the first notification.

I logged in to check, and it stopped.

Went to play some games and … 30 minutes later, I got the second notification.

That’s when it clicked – the timing of the attacks wasn’t random. They were checking my Discord status to see if I was online.

Sure enough, I confirmed this by setting myself as offline on Discord, and the attacks popped right back up.

Over the next few days, I used this insight to mess with the hackers and to use them as my personal pen testers.

I didn’t want to remove free credits for everyone, so I began experimenting with different ways to deter future attackers.

I would make a change, then go ‘offline’. I’d watch them troubleshoot their automations until they figured out a workaround. Then I’d go back ‘online’. The attacks would resume, and the cycle would repeat.

I mostly forgot about the entire incident until they came back to try their luck again the following week. But despite a few nights of alerts about tripwires getting triggered, they never managed to get more than the few cents we deposited into new accounts – not enough of an incentive to keep trying.

In the end, it was the cat-and-mouse game that made the whole experience worth it. I got free pen testing; they got a few dollars.

Card testing vulnerability

I wasn’t surprised about the overdraft feature being abused. This was something we were aware of and treated as a conscious trade-off between convenience and risk of abuse.

The bigger issue was that this made me realize that a malicious actor could abuse our system for card testing. That’s a widespread problem and one that will get your Stripe account flagged. When researching this problem, I didn’t find many effective solutions, so I wanted to dedicate part of this blog post to sharing what I learned.

Here’s what I tried and how it held up:

Method Effectiveness
Device fingerprinting Ineffective. Fingerprints are great for detecting legitimate returning users (e.g. to bypass CAPTCHA), but because they are easy to fake, they are not effective at detecting malicious actors.
IP address blocking Ineffective. Residential proxies are cheap and easy to get.
CAPTCHA Mild deterrent. Ineffective. Many existing solutions to bypass CAPTCHA.
OTP Mild deterrent. Ineffective. Many existing solutions to bypass OTP.
JA4 Somewhat effective. JA4 is a TLS fingerprinting method that identifies clients based on how they negotiate TLS connections. Of all data points that we collect, JA4 is the most stable identifier.
ALTCHA Somewhat effective. ALTCHA is a proof-of-work challenge that requires the client to solve a computational puzzle before submitting a request. When combined with prior methods, can slow down the attacks enough to deter the attacker.
Rate limiting Somewhat effective. Slows down the attacks, but may hurt legitimate users.

At the end of the day, each method is individually bypassable – the game is making the combination expensive enough that the attacker moves on.

Oh, and set your Discord status to offline.

Your Conversations Are Not Gone

I had a conversation with Claude last week that I did not want to lose.

We were planning a major overhaul to how my AI collaborator handles session continuity — memory, checkpoints, state transfer between sessions. It was a 45-message back-and-forth where we brainstormed, debated trade-offs, rejected approaches, and landed on an architecture. The kind of conversation where the reasoning matters as much as the result.

Then I cleared the context and moved on. The plan was captured in a handoff document, but when the next session tried to implement it, things went sideways. The handoff had the decisions but not the reasoning. It had the “what” but not the “why not.” The next version of Claude could not make the same judgment calls because it did not have the same context.

This morning I learned something: those conversations are not actually gone.

Claude Code saves everything

Every Claude Code session is automatically saved as a JSONL transcript file on your machine. No configuration required, no extra cost, no additional API usage. It just happens.

The files live at ~/.claude/projects/<project-hash>/<session-id>.jsonl and contain every message, tool call, and result from the session. They persist on your machine as long as you keep them.

I had 150 session transcripts sitting on disk and did not know it.

The raw files are not useful on their own

A 2MB JSONL file full of tool calls, system messages, and metadata is not something you want to read. The lifecycle planning session I was looking for was 1.9MB of JSONL, but the actual conversation — just the messages between me and Claude — was about 148KB. Still a lot, but manageable.

The useful part is the human-readable conversation stripped of everything else. User messages and assistant text, in order, with the tool machinery removed.

A skill to find and save them

I built a Claude Code skill called /load-transcript that does two things:

  1. Search — find sessions by keyword, date, or session ID. It scans the JSONL files and shows matching sessions with hit counts.
  2. Save — extract the conversation from a session and save it as a clean markdown file in a transcripts/ directory. Date-stamped, descriptively named, permanently searchable.

The idea is not to save every session. Most are routine. The idea is that when you have a conversation worth keeping — a planning session, a deep architectural debate, a brainstorm that produced something good — you can archive it before the retention window closes.

Why this matters

The gap between “planning” and “executing” is one of the biggest friction points in working with AI. You have a great collaborative session where you hash out an approach. Then you move to implementation, often in a new session with fresh context. The plan document captures the output, but the conversation that produced it contained something the document does not: the rejected alternatives, the trade-offs you considered, the moments where you changed your mind and why.

When implementation hits an ambiguous decision point, the plan says “do X.” The conversation would have said “we considered Y and Z, rejected Y because of this constraint, and chose X because it handles this edge case better.” That context is the difference between an implementer who can make good judgment calls and one who follows instructions blindly.

Session transcripts are not a perfect solution to this. Loading a 45-message conversation into a new session is a lot of context. But having it available — searchable, readable, referenceable — is better than having it silently expire after 30 days.

The broader pattern

This is part of something I keep coming back to: the biggest gap in working with AI is not capability. It is state. Context is the bottleneck. The model is smart enough. The question is whether it has what it needs to make the right call.

Every tool I build for my workflow — memory systems, continuity rules, checkpoint mechanisms, and now transcript archiving — is an attempt to solve the same problem from a different angle. How do you give an AI collaborator the context it needs, when it needs it, without overwhelming it?

I do not have a complete answer yet. But I know that letting good conversations disappear is not it.

How to Scrape Bluesky Posts: AT Protocol Public Data Extraction with Python

Bluesky is the decentralized social network built on the AT Protocol. Unlike traditional platforms, the AT Protocol is designed to be open — making public data freely accessible without authentication. This is a game-changer for data extraction.

Here’s how to scrape Bluesky posts and profiles using Python.

Why Bluesky Data is Special

  • Open by design: The AT Protocol makes public data accessible via standard APIs
  • No auth required: Public posts, profiles, and feeds are openly available
  • Growing fast: Millions of users migrating from Twitter/X
  • Rich data: Posts, replies, likes, reposts, follows — all accessible
  • Decentralized: Data is portable and not locked behind one corporation

Understanding the AT Protocol

Each user has a data repository identified by their DID (Decentralized Identifier). The public API endpoints let you read this data directly.

import requests
import json
import time

BSKY_PUBLIC_API = "https://public.api.bsky.app"

def resolve_handle(handle):
    """Convert a Bluesky handle to a DID."""
    url = f"{BSKY_PUBLIC_API}/xrpc/com.atproto.identity.resolveHandle"
    response = requests.get(url, params={"handle": handle})
    if response.status_code == 200:
        return response.json().get("did")
    return None

def get_profile(handle_or_did):
    """Get a user's profile information."""
    url = f"{BSKY_PUBLIC_API}/xrpc/app.bsky.actor.getProfile"
    response = requests.get(url, params={"actor": handle_or_did})
    if response.status_code != 200:
        return None
    data = response.json()
    return {
        "did": data.get("did"),
        "handle": data.get("handle"),
        "display_name": data.get("displayName", ""),
        "followers_count": data.get("followersCount", 0),
        "follows_count": data.get("followsCount", 0),
        "posts_count": data.get("postsCount", 0),
    }

Fetching User Posts

def get_author_feed(handle_or_did, limit=50):
    """Get posts from a specific user."""
    url = f"{BSKY_PUBLIC_API}/xrpc/app.bsky.feed.getAuthorFeed"
    all_posts = []
    cursor = None

    while len(all_posts) < limit:
        params = {"actor": handle_or_did, "limit": 30}
        if cursor:
            params["cursor"] = cursor

        response = requests.get(url, params=params)
        if response.status_code != 200:
            break

        data = response.json()
        feed = data.get("feed", [])
        if not feed:
            break

        for item in feed:
            post = item.get("post", {})
            record = post.get("record", {})
            all_posts.append({
                "uri": post.get("uri", ""),
                "author_handle": post.get("author", {}).get("handle", ""),
                "text": record.get("text", ""),
                "created_at": record.get("createdAt", ""),
                "like_count": post.get("likeCount", 0),
                "repost_count": post.get("repostCount", 0),
                "reply_count": post.get("replyCount", 0),
            })

        cursor = data.get("cursor")
        if not cursor:
            break
        time.sleep(0.5)

    return all_posts[:limit]

Searching Bluesky Posts

def search_posts(query, limit=50):
    """Search for posts containing specific terms."""
    url = f"{BSKY_PUBLIC_API}/xrpc/app.bsky.feed.searchPosts"
    all_results = []
    cursor = None

    while len(all_results) < limit:
        params = {"q": query, "limit": 25}
        if cursor:
            params["cursor"] = cursor

        response = requests.get(url, params=params)
        if response.status_code != 200:
            break

        data = response.json()
        posts = data.get("posts", [])
        if not posts:
            break

        for post in posts:
            record = post.get("record", {})
            all_results.append({
                "uri": post.get("uri"),
                "author": post.get("author", {}).get("handle"),
                "text": record.get("text", ""),
                "created_at": record.get("createdAt"),
                "likes": post.get("likeCount", 0),
                "reposts": post.get("repostCount", 0),
            })

        cursor = data.get("cursor")
        if not cursor:
            break
        time.sleep(0.5)

    return all_results[:limit]

Building a Bluesky Monitor

def monitor_keywords(keywords, interval_seconds=300):
    """Monitor Bluesky for specific keywords."""
    seen_uris = set()
    while True:
        for keyword in keywords:
            results = search_posts(keyword, limit=25)
            new_posts = [p for p in results if p["uri"] not in seen_uris]
            for post in new_posts:
                seen_uris.add(post["uri"])
                print(f"[NEW] @{post['author']}: {post['text'][:100]}")
        print(f"Checked {len(keywords)} keywords, sleeping {interval_seconds}s...")
        time.sleep(interval_seconds)

monitor_keywords(["web scraping", "data extraction", "python scraper"])

Scaling Bluesky Scraping

For production-scale Bluesky data collection, the Bluesky Scraper on Apify handles the heavy lifting with pagination, rate limits, and data normalization.

For proxy management when making high-volume API calls, ScrapeOps provides rotating proxy infrastructure that works perfectly with the AT Protocol endpoints.

Best Practices

  1. No auth needed: Public data is freely available — don’t over-complicate it
  2. Use the public API: public.api.bsky.app is the correct endpoint
  3. Rate limit gently: 0.5-1 second between requests
  4. Use ScrapeOps for proxy rotation at scale

Conclusion

Bluesky’s open AT Protocol makes it the most scraper-friendly social network today. Whether you use the public API directly or the Bluesky Scraper on Apify, the data is readily accessible for analysis.

Happy scraping!

Open-Meteo Has a Free Weather API — No Key, No Signup, Real Forecast Data

Most weather APIs hide behind registration walls, throttle free accounts heavily, or charge immediately for anything useful. Open-Meteo is different: completely free, no API key, no account, open-source, and it returns real hourly/daily forecasts.

One URL, no auth:

https://api.open-meteo.com/v1/forecast?latitude=40.7128&longitude=-74.0060&current=temperature_2m,wind_speed_10m

Try it right now:

curl "https://api.open-meteo.com/v1/forecast?latitude=40.7128&longitude=-74.0060&current=temperature_2m,wind_speed_10m,relative_humidity_2m,weather_code"

Response:

{
  "latitude": 40.710335,
  "longitude": -73.99235,
  "timezone": "GMT",
  "current": {
    "time": "2026-03-26T16:15",
    "temperature_2m": 18.0,
    "wind_speed_10m": 19.0,
    "relative_humidity_2m": 50,
    "weather_code": 0
  },
  "current_units": {
    "temperature_2m": "°C",
    "wind_speed_10m": "km/h",
    "relative_humidity_2m": "%",
    "weather_code": "wmo code"
  }
}

No API key header. No rate limit response. No credit card.

How it works: latitude + longitude + variables

Every request needs two parameters: latitude and longitude. Everything else is optional — you specify which variables you want.

Current weather variables:

  • temperature_2m — air temperature at 2m height (°C or °F)
  • wind_speed_10m — wind speed at 10m (km/h, mph, m/s, or knots)
  • relative_humidity_2m — humidity (%)
  • precipitation — precipitation in the last hour (mm)
  • weather_code — WMO weather code (0=clear, 1-3=partly cloudy, 45=fog, 61-67=rain, 71-77=snow, etc.)
  • apparent_temperature — feels-like temperature
  • is_day — 1 if daytime, 0 if night

Hourly forecast variables (up to 16 days):

  • temperature_2m, precipitation_probability, wind_speed_10m
  • uv_index, visibility, soil_temperature_0cm

Python: current weather for any city

import requests

def get_weather(lat: float, lon: float) -> dict:
    url = "https://api.open-meteo.com/v1/forecast"
    params = {
        "latitude": lat,
        "longitude": lon,
        "current": "temperature_2m,apparent_temperature,wind_speed_10m,relative_humidity_2m,weather_code,precipitation",
        "wind_speed_unit": "mph",
        "temperature_unit": "fahrenheit"
    }
    response = requests.get(url, params=params)
    response.raise_for_status()
    return response.json()["current"]

# New York City
weather = get_weather(40.7128, -74.0060)
print(f"Temperature: {weather['temperature_2m']}°F (feels like {weather['apparent_temperature']}°F)")
print(f"Wind: {weather['wind_speed_10m']} mph")
print(f"Humidity: {weather['relative_humidity_2m']}%")
print(f"Precipitation: {weather['precipitation']} mm")

Output:

Temperature: 64.4°F (feels like 61.2°F)
Wind: 11.8 mph
Humidity: 50%
Precipitation: 0.0 mm

Get an hourly 3-day forecast

import requests
from datetime import datetime

def get_forecast(lat: float, lon: float, days: int = 3) -> list[dict]:
    url = "https://api.open-meteo.com/v1/forecast"
    params = {
        "latitude": lat,
        "longitude": lon,
        "hourly": "temperature_2m,precipitation_probability,wind_speed_10m",
        "forecast_days": days,
        "temperature_unit": "fahrenheit"
    }
    data = requests.get(url, params=params).json()
    hourly = data["hourly"]

    # Zip into list of dicts
    return [
        {
            "time": t,
            "temp_f": temp,
            "precip_pct": precip,
            "wind_mph": wind
        }
        for t, temp, precip, wind in zip(
            hourly["time"],
            hourly["temperature_2m"],
            hourly["precipitation_probability"],
            hourly["wind_speed_10m"]
        )
    ]

forecast = get_forecast(40.7128, -74.0060)
for hour in forecast[:6]:  # First 6 hours
    print(f"{hour['time']}: {hour['temp_f']}°F, {hour['precip_pct']}% rain chance, {hour['wind_mph']} mph")

Output:

2026-03-26T00:00: 47.2°F, 5% rain chance, 6.3 mph
2026-03-26T01:00: 45.8°F, 4% rain chance, 5.8 mph
2026-03-26T02:00: 44.5°F, 3% rain chance, 5.1 mph
2026-03-26T03:00: 43.7°F, 3% rain chance, 4.9 mph
2026-03-26T04:00: 42.9°F, 2% rain chance, 5.2 mph
2026-03-26T05:00: 42.4°F, 2% rain chance, 5.5 mph

Historical weather data (also free)

Open-Meteo has a separate endpoint for historical data, also free:

import requests

def get_historical(lat: float, lon: float, start: str, end: str) -> dict:
    url = "https://archive-api.open-meteo.com/v1/archive"
    params = {
        "latitude": lat,
        "longitude": lon,
        "start_date": start,
        "end_date": end,
        "daily": "temperature_2m_max,temperature_2m_min,precipitation_sum,wind_speed_10m_max"
    }
    return requests.get(url, params=params).json()

# Get last week's data for London
history = get_historical(51.5074, -0.1278, "2026-03-19", "2026-03-25")
daily = history["daily"]
for date, max_t, min_t, precip in zip(
    daily["time"],
    daily["temperature_2m_max"],
    daily["temperature_2m_min"],
    daily["precipitation_sum"]
):
    print(f"{date}: {min_t}–{max_t}°C, {precip}mm rain")

The archive goes back to 1940 for most locations. No key. No account.

JavaScript: weather widget in the browser

async function getCurrentWeather(lat, lon) {
  const params = new URLSearchParams({
    latitude: lat,
    longitude: lon,
    current: 'temperature_2m,apparent_temperature,weather_code,wind_speed_10m',
    temperature_unit: 'fahrenheit'
  });

  const response = await fetch(
    `https://api.open-meteo.com/v1/forecast?${params}`
  );
  const data = await response.json();
  return data.current;
}

// Works directly from the browser — no CORS issues
getCurrentWeather(34.0522, -118.2437).then(weather => {
  console.log(`LA: ${weather.temperature_2m}°F, code: ${weather.weather_code}`);
});

No CORS issues — Open-Meteo has Access-Control-Allow-Origin: *.

What the weather codes mean

The weather_code field uses WMO weather interpretation codes:

WMO_CODES = {
    0: "Clear sky",
    1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast",
    45: "Fog", 48: "Icy fog",
    51: "Light drizzle", 53: "Moderate drizzle", 55: "Heavy drizzle",
    61: "Slight rain", 63: "Moderate rain", 65: "Heavy rain",
    71: "Slight snow", 73: "Moderate snow", 75: "Heavy snow",
    80: "Slight showers", 81: "Moderate showers", 82: "Heavy showers",
    95: "Thunderstorm",
}

def describe_weather(code: int) -> str:
    return WMO_CODES.get(code, f"Unknown ({code})")

print(describe_weather(0))   # Clear sky
print(describe_weather(63))  # Moderate rain

Free vs. paid (commercial)

Open-Meteo is free for non-commercial use. The limits:

Tier Cost Rate limit Use case
Free $0 ~10,000 req/day Personal projects, apps, research
Commercial From $29/mo Higher limits + SLA Production apps

For most developer projects — dashboards, personal apps, learning — the free tier is more than enough.

What it doesn’t do

  • No severe weather alerts (use NWS API for US alerts)
  • No minute-by-minute nowcasting (free tier updates hourly)
  • No air quality data (separate API at air-quality-api.open-meteo.com, also free)

Need weather data at scale or automated collection? Email [email protected] for custom data pipelines, or check my Apify actors for ready-made scrapers.

Quarkus 3 application on AWS Lambda- Part 10 Measuring Lambda cold and warm starts with REST API application

Measurements of cold and warm start times of our application

In Part 8, we learned how to develop a pure Quarkus REST application and deploy it on AWS Lambda.

For the preparation of my talk about developing Serverless Java REST applications on AWS using frameworks such as Quarkus, I found time to measure the performance of the Lambda function with the different approaches (see below). I refer to my articles where we didn’t use the Quarkus REST controllers directly, but the pure AWS Lambda functions, to see how we implemented the SnapStart and priming approaches. They look completely the same when using Quarkus REST controllers.

  1. No SnapStart. Please read the article Introduction to the sample application and first Lambda performance measurements.
  2. SnapStart enabled, but no priming applied. Please read the article Reducing Lambda cold starts with Lambda SnapStart.
  3. SnapStart enabled, and DynamoDB request priming applied. Please read the article Reducing Lambda cold starts with SnapStart and DynamoDB request priming.
  4. SnapStart enabled, and API Gateway request event priming applied. Please read the article Reducing Lambda cold starts with SnapStart and API Gateway request event priming.

Cold (c) and warm (w) start time with -XX:+TieredCompilation -XX:TieredStopAtLevel=1 compilation in ms:

Scenario Number c p50 c p75 c p90 c p99 c p99.9 c max w p50 w p75 w p90 w p99 w p99.9 w max
No SnapStart enabled 4586 4697 4855 5073 5384 5389 6.51 7.39 9.08 22.45 101.41 1747
SnapStart enabled but no priming applied, all 1979 2049 2816 2916 2990 2991 6.51 7.51 9.08 22.09 106.35 2134
SnapStart enabled but no priming applied, last 70 1953 2016 2098 2278 2278 2278 6.51 7.51 9.08 22.09 78.66 1673
SnapStart enabled and DynamoDB request priming applied, all 1017 1057 2013 2041 2043 2045 6.30 7.27 8.94 21.07 56.36 1273
SnapStart enabled and DynamoDB request priming applied, last 70 1017 1040 1095 1177 1177 1177 6.30 7.27 8.94 21.07 47.44 629
SnapStart enabled and API Gateway request event priming applied, all 747 801 1092 1258 1310 1311 6.51 7.63 9.53 21.75 62.00 304
SnapStart enabled and API Gateway request event priming applied, last 70 735 766 841 1061 1061 1061 6.51 7.63 9.34 21.75 55.48 261

What we observe is that both cold and warm start times of the sample application using Quarkus REST controllers are generally higher when using the pure AWS Lambda functions. I suspect the reason for it lies in an extra adapter layer used to convert the Lambda request to the REST controller. This adapter uses several extra dependencies, which leads to a much bigger artifact size, which is also one of the factors of the increased cold start times. The advantage of using the REST controller is the ability to more easily migrate the business logic from AWS Lambda to EC2 or Containers.

Take your vibe coding to the next level

If you’ve been following this series, you know that Cloud Run makes deployment easy and AI tools make development fast. But there is a new frontier in software engineering that combines both into a single, fluid experience: Vibe coding.

Vibe coding isn’t about being “lazy”; it’s about operating at a higher level of abstraction. It’s the ability to stay in a “flow state” where you describe your vision, and the AI handles the syntax, the boilerplate, and the deployment architecture on Cloud Run. In this final part, we look at how to master this high-velocity workflow without losing the “pro” edge.

Turn your ideas into a PRD

This video demonstrates generating user personas, core features, and success metrics, and then scaffolding an app structure to deploy on Cloud Run.

Architect your startup strategy

Learn how to generate a “best practices.md” file that covers stateful containers, structured logging, and CI/CD rules for your Cloud Run deployments.

Build and deploy production-ready features

Watch a production-ready API feature be built and deployed in under a minute using the Gemini CLI.

Vibe debugging

See how the AI can read production logs, identify a null pointer exception, and write a fix so you can get your app back online quickly.

Get started today!

The cloud is no longer a place you “send” your code; it’s an extension of your development environment. By mastering these workflows, you aren’t just a developer—you’re an architect of the future.

My First Experience Using Excel for Real-World Data Analysis.

Before starting this project, I honestly knew almost nothing about Excel. I had heard about it, seen people use it, but I never really understood how powerful it is. As just a girl trying to figure things out, I was honestly a bit nervous and overwhelmed at first. But at the same time, I was open to learning and ready to take on the challenge. Like many beginners, I saw it as just a tool for storing data in rows and columns. However, through hands-on practice, I quickly realized that Excel is far more powerful—it is a complete data analysis tool used by professionals across industries.

This article shares how Excel is applied in real-world data analysis, based on my experience building a product performance dashboard.

What is Excel?
Microsoft Excel is a tool used to organize, clean, analyze, and present data. It works using rows and columns, and it has features like formulas, PivotTables, charts, and slicers that help turn raw data into meaningful information. Even in 2026, Excel remains one of the most widely used tools for data analysis due to its accessibility and versatility

At first, it looked complicated—but as I started using it, I slowly began to understand how everything connects.

🌍 Real-World Use

I learned that Excel is used almost everywhere—in business, marketing, finance, and even e-commerce. For example, a company like Jumia can use Excel to analyze:

  • Product prices
  • Discounts
  • Customer reviews
  • Ratings

This helps them make decisions like:
Which products to promote
Whether to increase or reduce prices
Which products need improvement

Seeing this made me realise that Excel is not just about numbers—it actually helps businesses grow.

🛠️ What I Learned (As a Beginner)

Even though I started from zero, I managed to learn and use several important Excel tools:

🔹 Data Cleaning
This was one of the hardest parts at first, but also the most important.
I used:
Text to Columns
Find & Replace
Functions like LEFT(), VALUE(), and ABS()
This helped me:
Remove “KSh” and commas
Convert text into numbers
Fix negative review values
Extract ratings from text

🔹 Data Enrichment
I created new columns using formulas:
Discount Amount
Rating Category (Poor, Average, Excellent)
Discount Category (Low, Medium, High)
At first, formulas looked scary—but once I understood them, they became really useful.

🔹 PivotTables
This was honestly one of the most powerful tools I learned. It made analyzing data much faster and easier.
I used PivotTables to:
Find averages
Identify top products
Group data into categories

🔹 Charts & Visuals
I created:
Bar charts for top products
Donut charts for categories
Scatter plots to see relationships
This is where everything started to make sense visually.

🔹 Slicers & Interactivity
This part felt really cool 😄
I added slicers so the dashboard can filter data instantly.
It made everything interactive and more professional.

🔹 Conditional Formatting
I used colors to highlight:
High discounts (green)
Low ratings (red)
This made important insights easy to spot.

💡 Personal Reflection

This experience completely changed how I see data.
At the beginning, I was confused and honestly a bit stressed because everything felt new. But as I kept practicing, I started understanding patterns and relationships.
Now I can look at data and think:
Why is this happening?
What does this mean?
What decision can be made from this?

For example, I noticed that:
Medium discounts sometimes have better ratings
High discounts don’t always mean more reviews

That really surprised me.

I’m still a beginner, and I know I have a lot to learn but now I feel more confident. This is just the start of my journey into data analysis, and I’m excited to keep improving.

Revenium Introduces AI Outcomes to Measure ROI at the Agentic Workflow Level

HERNDON, Va. —  Revenium, the AI Economic Control System, today announced the launch of AI Outcomes, a new solution linking every AI agent execution to its business outcome and calculating ROI at the workflow level. Where previously-released Revenium tools answered the question of what AI spent across machine and human workflows, AI Outcomes answers whether that spend … continue reading

The post Revenium Introduces AI Outcomes to Measure ROI at the Agentic Workflow Level appeared first on SD Times.

cathedral-memory is now on npm — persistent memory for JS/TS AI agents

Your AI agent forgets everything when the context resets. Cathedral fixes that.

The official JS/TypeScript SDK is now live on npm:

npm install cathedral-memory

Zero dependencies. Node.js 18+. CommonJS and ESM both supported. Full TypeScript types included.

Quickstart

import { CathedralClient } from "cathedral-memory";

const { client } = await CathedralClient.register("MyAgent", "A helpful assistant");

const ctx = await client.wake();
console.log(ctx.core_memories);

await client.remember({
  content: "User prefers dark mode",
  category: "relationship",
  importance: 0.8
});

What’s included

  • wake() — full memory reconstruction at session start
  • remember() / memories() — store and semantic search
  • drift() — tamper detection
  • snapshot() — named checkpoints
  • logBehaviour() — audit trail
  • compact() — prune stale memories
  • verifyExternal() — cross-check with external behavioural data

Try live (no signup): https://cathedral-ai.com/playground

Python: pip install cathedral-memory — same API.

GitHub: https://github.com/AILIFE1/cathedral-ai-cathedral-js-