An aviation market intelligence app, built in an evening

Rafael
•

Every industrial company has someone whose morning starts with the news. A strategy team wants to know which competitor won which order. An asset manager wants to know which fleets or plants are being retired. A supplier's sales leads want to know which programme just slipped. The person who answers them skims dozens of newsrooms, trade titles and regulator pages, sees the same press release three times, and writes a brief and a slide deck by hand.

We wanted to know how long it takes to build the thing that does the reading and the first draft. We tried it on aviation news, on Curiosity, with a coding agent (Claude Code) doing the building.

First commit: 21:56 UTC. Last commit of the night: 02:43, with the application ingesting the real feeds, linking entities and rendering reports. The next morning, one more hour to deploy it to a hosted workspace. The whole project is public at curiosity-ai/aviation-intelligence-demo.

Total build time: one evening.

One question for an aviation analyst: what changed today?

The application answers one question for an aviation market analyst: what changed in commercial aviation and aerospace, and what does it mean for us? It does that every day and every week.

It reads from a directory of 248 real RSS feeds in 15 categories: airline and manufacturer newsrooms, trade titles, regulators, airports, MRO, leasing, defence and space. Each row says what the feed is for, how far to trust it (Core, Secondary, Low frequency) and whether its articles can be read in full or only as a teaser. An analyst curated that directory before we started.

From there the application:

  • fetches every feed and reads each new article in full where the publisher allows it, recording when it could only use the feed's summary;
  • finds the companies, aircraft types, engines, launch vehicles and airports each article mentions;
  • shows the news desk on a home page, where every number opens the articles it counted;
  • writes a daily brief and a weekly report, each published as an HTML page and a PowerPoint deck, with every item linked to the article it came from.
The landing page this morning: 423 articles in the last 24 hours and 1,215 in seven days, feed health, the week by category and articles per day. The Daily brief and Weekly report buttons open a chat with the request already sent.

From 248 feeds to a graph of companies and aircraft

flowchart LR D[Feed directory<br/>248 feeds] --> C[Connector] V[Vocabularies<br/>Wikidata, OurAirports] --> C C -->|feeds, articles,<br/>publishers, tags| G[(Workspace graph)] G --> N[Entity recognition] N -->|_Mentions| G G --> S[News desk sandbox] S --> A[Assistant and agent] A --> R[HTML brief<br/>+ PowerPoint deck] G --> F[Front end]

The connector is a .NET console application. It fetches every feed, groups items by their canonical URL so a press release carried by four titles is one article, loads each new page in headless Chromium and extracts the text with Readability. It writes only what the sources state: the feed, the publisher, the category, the publisher's own tags. On its full run 234 of the 248 feeds answered, and of 1,524 articles from the last 14 days, 1,276 were read from the page, 97 were probably a paywall teaser, 71 came from the feed's own full text and 80 had only a summary.

What an article is about is not written by the connector. That is left to the workspace, because a demo that copies the answer in from the feed proves nothing. The connector loads five vocabularies generated from Wikidata and OurAirports (2,073 organisations, 1,285 aircraft types, 594 engine types, 251 launch vehicles and 1,151 airports), with every term that should match as an alias. The workspace builds a spotter from each and links every article's title and body to the entities it finds. Boeing is mentioned in 130 articles, Airbus in 81, NASA in 157. Getting those numbers right was most of the second half of the evening: "Falcon" is a rocket and a business jet, "Emirates" is an airline and a country, "LEAP" is an engine and a verb, and each false match was fixed by changing the rule that produced it, not the entry.

An article with the entities the workspace found in it: 14 aircraft types and 5 organisations, each a tab that opens the other articles that mention it.

The front end is C# compiled to JavaScript with Transpose and built from Tesserae components. It replaces the workspace's home page with the news desk, lists the feeds with their health, renders every node type, and pins the chat to the market intelligence assistant. When the assistant answers, the companies and airports it names are matched against the same vocabularies and shown as links.

The assistant, its report skill and the news desk sandbox

The AI layer is four files of workspace configuration. The assistant is a system prompt plus the tools, agent and skill it may use. This is the whole of it:

aviation-market-intelligence-AVAssistant11111111111.cs
[assistant: Curiosity.Assistants.UID("AVAssistant11111111111")]
[assistant: Curiosity.Assistants.Name("Aviation Market Intelligence")]
[assistant: Curiosity.Assistants.PublicAccess("true")]
[assistant: Curiosity.Assistants.Tool("AVsandboX1111111111111")]
[assistant: Curiosity.Assistants.Tool("ConsuLt111111111111111")]
[assistant: Curiosity.Assistants.Tool("LLMSearCh1111111111111")]
[assistant: Curiosity.Assistants.Agent("AVAgent111111111111111")]
[assistant: Curiosity.Assistants.Skill("Avreport11111111111111")]

You are the aviation market intelligence assistant. You help analysts follow commercial
aviation and aerospace through the news this workspace monitors: 248 RSS feeds from airlines,
manufacturers, MRO and supply-chain publications, airports, regulators, business aviation,
defence and space sources, each article read in full where the publisher allows it.

# What you can do

- **Answer questions about the news** - what happened, who announced what, what a regulator
  decided, how a story developed over the week. Use the `news_desk` sandbox (`news`, `article`,
  `stats`, `grep` over `/news`) or `search` / `consult`. Read an article before you describe it.
- **Brief on a company, programme or topic** - collect every relevant article in the window,
  then answer with the developments and their implications, each with its source.
- **Write the daily brief or the weekly report** - follow the `aviation-market-reports` skill
  and publish the HTML page and the PowerPoint deck from the `news_desk` templates. For a long
  window, or when asked to do it in the background, hand it to the Aviation Market
  Intelligence Analyst agent; it leaves the two pages in its own `/agents/...` directory, so
  copy them into `/artifacts` and `publish` them yourself.
- **Explain the coverage** - which feeds are monitored, which failed on their last fetch, which
  publishers are summary-only (`/feeds/directory.tsv`, `stats`).

# How to answer

- Lead with the answer. Then the evidence: each claim with its article, linked.
- Say when something is only known from a feed summary, when sources disagree, and when the
  monitored feeds do not cover a question - never fill a gap from memory.
- Use absolute dates. Quote figures as the source states them.
- Keep chat answers short; when the answer is a deliverable, make it an artifact.

The editorial method lives in a separate skill, so the assistant and the agent follow the same one: which feeds lead a section, how to merge one story told by several publishers, and that every item needs a "so what" and a source.

The tool that does the work is the Aviation News Desk, a sandbox. We wrote about sandboxes in the Computerwelt series: how the bash and Python sandbox was built, how Sudo replaced seventy-four tools with one shell, and how to give your own agent one. In short, an AI tool returns an ISandbox, and the model gets a shell over a filesystem the tool builds. Here is the part of the news desk that declares its filesystem and commands:

AVsandboX1111111111111.cs (excerpt)
public class AviationNewsDesk : ISandbox
{
    public string FunctionName => "news_desk";

    public string HomeDirectory => "/work";

    public SandboxMount[] Mounts => new[]
    {
        SandboxMount.Lazy("/news", "articles by day as markdown, README.md and a 7-day index.tsv", ListNews, ReadNews),
        SandboxMount.Live("/feeds", "the feed directory: directory.tsv plus one markdown file per feed", ReadFeeds),
        SandboxMount.Files("/templates", "report templates: report.html, deck.html, the JSON contract in README.md and a daily and a weekly sample",
            SandboxFile.Text("README.md", TemplatesReadme),
            SandboxFile.Text("report.html", ReportTemplate),
            SandboxFile.Text("deck.html", DeckTemplate),
            SandboxFile.Text("sample-daily.json", SampleDaily), SandboxFile.Text("sample-weekly.json", SampleWeekly)),
        SandboxMount.Scratch("/work", "scratch space for report JSON, notes and drafts"),
    };

    public Mosaik.Schema.ArtifactType[] Artifacts => new[] { Mosaik.Schema.ArtifactType.Html, Mosaik.Schema.ArtifactType.Pdf };

    public SandboxQueryAccess Query => SandboxQueryAccess.For(
        N.Article.Type, N.Feed.Type, N.Publisher.Type, N.FeedCategory.Type, N.SourceType.Type, N.Priority.Type, N.Tag.Type,
        N.Organisation.Type, N.AircraftType.Type, N.EngineType.Type, N.LaunchVehicle.Type, N.Airport.Type).WithMaxNodes(200);

    public SandboxCommand[] Commands => new[]
    {
        SandboxCommand.Create("news",
            "news [--since 1d|7d|YYYY-MM-DD] [--until YYYY-MM-DD] [--category <text>] [--priority Core|Secondary] [--publisher <text>] "
          + "[--feed <text>] [--lang <code>] [--grep <text>] [--mentions <name>] [--content page|summary] [--limit N] [--json]: ...",
            call => Task.FromResult(News(call))),

        SandboxCommand.Create("article",
            "article <uid> [<uid> ...]: the full article as markdown - metadata, feeds, excerpt and body. ...",
            call => /* RenderArticle for each uid */),

        SandboxCommand.Create("stats",
            "stats [--since 1d|7d|YYYY-MM-DD] [--until YYYY-MM-DD] [--mentions <name>]: article counts by category, priority, publisher, day "
          + "and content source, the most-mentioned entities (top_mentions), plus feed health. ...",
            call => Task.FromResult(Stats(call))),

        SandboxCommand.Create("render-report",
            "render-report report|deck <data.json> [<name>]: fills /templates/report.html or /templates/deck.html with the report JSON ... "
          + "Checks the JSON against the contract in /templates/README.md first and refuses a report with missing sections or sources. ...",
            RenderReportAsync),

        SandboxCommand.Search(new[] { N.Article.Type, N.Feed.Type, N.Publisher.Type, N.Organisation.Type, N.AircraftType.Type, N.EngineType.Type, N.LaunchVehicle.Type, N.Airport.Type }),
        SandboxCommand.Consult(new[] { N.Article.Type, N.Feed.Type, N.Publisher.Type }),
    };

    // ...the readers behind each mount and command, and the embedded templates
}

The articles appear as one Markdown file per article under /news/days/<date>/, read only when something opens them. The commands cover what a file listing cannot: news filters the whole corpus by date, category, priority or the entity an article mentions; stats counts it; render-report checks a report against its contract before it renders. One report JSON becomes both the HTML page and the deck, so the two never disagree.

The daily brief the assistant wrote this morning

The hosted workspace has a model configured (deepseek-v4.1-flash), so this morning we pressed Daily brief on the landing page and watched. The request went in at 12:41 UTC. The report was published at 12:52.

In those eleven minutes the model read the report skill, then made 35 calls to the news desk. Each call is a shell script. It listed the last 24 hours by category, searched for orders, groundings, deliveries and fuel, followed entities with news --mentions airBaltic, read the candidate articles in full with article, compared its plan against the sample report in /templates, and wrote the report JSON to /work. Before publishing it went back to the source articles to check its headline figures. Every step is shown in the conversation and can be opened to see exactly what ran and what came back:

One step expanded: render-report validates the report JSON and writes the HTML page and the deck, 5 sections and 18 items, ready to publish.

The finished brief opens beside the conversation. The reply summarises the conclusion and says what the report could not cover: 29 feeds had failed on their last fetch, including SpaceNews and the Airbus press-release feeds, so first-party announcements from those publishers may be missing, and three Aviation Week items were only readable as paywalled opening paragraphs.

The conversation with the published daily brief open beside it.

Here is the brief itself, as the workspace published it. It leads with air cargo going into the fourth-quarter peak and has five sections: safety and regulation, airlines, cargo and logistics, manufacturers and supply chain, and defence and space. Each item links to its source article.

The daily brief for 7 October 2026, written by the assistant from the 427 articles published in the 24 hours before it started, as the workspace published it.

You can also open the brief on its own page.

Nobody edited it before it went into this post. That is also its limit: it is a first draft from what 219 working feeds said in one day, and the coverage notes are there so an analyst knows what to check before it goes any further.

Next: hourly feed fetches from inside the workspace

Today the connector runs on a machine next to the workspace, because reading the news well takes a browser. The next step is to run it inside the workspace itself, as a scheduled data connector imported with the rest of the configuration, so a hosted workspace keeps its corpus current with nothing else to run.

We tried it. Without a browser, the connector can only read pages with a plain request, and a first run from the server showed what that costs: 219 of the 248 feeds answered, against 234 for the browser-based run. 19 refused the request with HTTP 403 and 10 returned a bot-check page instead of the feed. Pages that only render their text with JavaScript fall back to the feed's summary.

That is why we are treating it as a future improvement rather than the way the demo runs today. We are bringing web browsing to the AI agents in a future update, and the same capability is what an in-workspace connector needs to read the news as well as the one that runs beside it.

What it took: five hours, one template, three human jobs

Measure Result
First build about 5 hours of commits, 21:56 to 02:43 UTC
Deployment to a hosted workspace about 1 hour
Feeds / articles in 14 days 248 / 1,524
Entities spotted from 5,354 vocabulary entries
Daily brief 11 minutes, 35 sandbox calls, 5 sections, 18 items
Model writing the reports deepseek-v4.1-flash

The agent did not start from a blank repository. It started from our application template: a CLAUDE.md with ten non-negotiables (never write a field name as a string, every metric is computed live, the connector writes only what the source states), and one skill per layer (connector, graph model, workspace configuration, endpoints, AI tools, front end, delivery), each pointing at a commented reference implementation. Both are in the repository. Most of the evening went into the parts that are specific to aviation news, the extraction and the vocabularies, because the rest had a worked example to adapt. When the agent verified something the skills did not cover, such as which libraries workspace code may import, it wrote it back into the skill for the next project.

Three things still needed a person. The feed directory was curated by an analyst. The vocabularies needed judgement about what counts as a match, and they still need more: "Viper" the engine still matches "Viper Shield". And the reports need someone to read them before they go to a customer, which is why every item carries its source.

If you have a question that is answered by reading a lot of documents every day, the pattern carries over: a curated list of sources, a connector that writes only what they state, entities the workspace finds on its own, and an agent that works through a sandbox shaped like the job. The repository has all of it, including the guide to running it against your own workspace.

Read next

Articles on context graphs, enterprise search and industrial AI

Connected knowledge for AI systems