<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Andrei Parfenov</title>
    <description>The latest articles on DEV Community by Andrei Parfenov (@andreiparfenov).</description>
    <link>https://dev.to/andreiparfenov</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F997249%2Fdd65825b-9751-4794-828c-f9430b42a367.jpg</url>
      <title>DEV Community: Andrei Parfenov</title>
      <link>https://dev.to/andreiparfenov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/andreiparfenov"/>
    <language>en</language>
    <item>
      <title>Building a Grounded LLM Planner for Swim Trainings on Nebius Serverless</title>
      <dc:creator>Andrei Parfenov</dc:creator>
      <pubDate>Wed, 24 Jun 2026 19:36:01 +0000</pubDate>
      <link>https://dev.to/andreiparfenov/building-a-grounded-llm-planner-for-swim-trainings-on-nebius-serverless-34fn</link>
      <guid>https://dev.to/andreiparfenov/building-a-grounded-llm-planner-for-swim-trainings-on-nebius-serverless-34fn</guid>
      <description>&lt;p&gt;Swimming is one of the few sports where structured training makes a measurable difference. Poor training structure slows progress and builds bad habits that are hard to undo. With the LA2028 Olympics approaching and a fresh wave of interest in competitive swimming, more amateur and age-group swimmers are taking their training seriously. The problem is that training plans have always lived behind a paywall: you either pay for an application or you guess.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdktgckyczqrgg5xpyy1l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdktgckyczqrgg5xpyy1l.png" width="800" height="533"&gt;&lt;/a&gt;Photo courtesy: Deepbluemedia&lt;/p&gt;

&lt;p&gt;A good training plan is a decision about how to distribute intensity across weeks, which drills to pair together in a session, and a coaching layer that explains why you're doing what you're doing. These are exactly the kinds of structured reasoning tasks that modern LLMs handle well, but only if they're grounded in real coaching knowledge. This project puts that kind of planning into a serverless API that any swimmer can use, and any coach can shape with their own expertise.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;GitHub repo: &lt;a href="https://github.com/andreiparfenov/swimming-coach-serverless" rel="noopener noreferrer"&gt;https://github.com/andreiparfenov/swimming-coach-serverless&lt;/a&gt; &lt;br&gt;
End user demo: &lt;a href="https://swimming-coach-iota.vercel.app" rel="noopener noreferrer"&gt;https://swimming-coach-iota.vercel.app&lt;/a&gt; &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I was planning to use Vercel or Lambda for this API, but their per-request timeouts don't work for a pipeline that runs for minutes, so I tried a long-running Serverless Endpoint instead.&lt;/p&gt;

&lt;p&gt;The system is built on two Nebius Serverless AI components working together: a &lt;strong&gt;Serverless Job&lt;/strong&gt; that builds a curated coaching knowledge base, and a &lt;strong&gt;Serverless Endpoint&lt;/strong&gt; that serves the actual planning API. Now, let’s walk through how each piece was actually built and deployed, commands included, so you can follow along or adapt it for your own project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwa94t2er8nmy5h0hpgkx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwa94t2er8nmy5h0hpgkx.png" width="800" height="778"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Build the planning API locally first
&lt;/h2&gt;

&lt;p&gt;Before touching any Nebius Serverless infrastructure, make sure the application is working entirely on your own machine. The app itself is a small FastAPI service with three chained agent calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_plan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SwimmerProfile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;TrainingPlan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;macro&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;run_periodization_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;weeks_raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;run_session_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;macro&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;weeks_enriched&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;run_coaching_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weeks_raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The periodization agent takes the swimmer's profile and decides the macro structure of the plan: week themes, intensity, and a volume curve. The session generator runs once per week and fills in the warmup, main set, and cooldown in pool lengths. The coaching notes agent batches every session into a single call and writes a short note for each one. Splitting the work this way, instead of asking a single prompt to do everything, keeps each call's output schema small and makes it far easier to work out which step has gone wrong when something does go wrong.&lt;/p&gt;

&lt;p&gt;To run it locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp &lt;/span&gt;env.example .env
&lt;span class="c"&gt;# fill in NEBIUS_API_KEY in .env&lt;/span&gt;

pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
uvicorn app.main:app &lt;span class="nt"&gt;--reload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you don't have a Token Factory API key, go to &lt;a href="https://tokenfactory.nebius.com" rel="noopener noreferrer"&gt;https://tokenfactory.nebius.com&lt;/a&gt;, sign in, and create a key under &lt;strong&gt;API keys&lt;/strong&gt;. It's only shown once, so copy it straight away. &lt;code&gt;NEBIUS_API_KEY&lt;/code&gt; is used to authenticate every agent call.&lt;/p&gt;

&lt;p&gt;Then test it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8000/generate-plan &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "level": "intermediate",
    "goal": "endurance",
    "sessions_per_week": 3,
    "session_duration_minutes": 60,
    "pool_length": 25,
    "weeks": 4
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Containerise and deploy the Endpoint
&lt;/h2&gt;

&lt;p&gt;Once it works locally, the next step is packaging it for Nebius.&lt;/p&gt;

&lt;p&gt;The Nebius CLI needs to be installed and authenticated (&lt;code&gt;nebius auth login&lt;/code&gt;), and Docker needs to be logged in to whichever registry you're pushing to (&lt;code&gt;docker login&lt;/code&gt;). Both are one-time setup steps.&lt;/p&gt;

&lt;p&gt;Let’s package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker buildx build &lt;span class="nt"&gt;--platform&lt;/span&gt; linux/amd64 &lt;span class="nt"&gt;-t&lt;/span&gt; docker.io/&amp;lt;your-username&amp;gt;/swimcoach-api:latest &lt;span class="nt"&gt;--push&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nebius Endpoints need a subnet to attach to, which can be picked up automatically if you only have one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;SUBNET_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;nebius vpc subnet list &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.items[0].metadata.id}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create the Endpoint itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nebius ai endpoint create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; swimcoach-api &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt; docker.io/&amp;lt;your-username&amp;gt;/swimcoach-api:latest &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--platform&lt;/span&gt; cpu-e2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--preset&lt;/span&gt; 2vcpu-8gb &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--public&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--container-port&lt;/span&gt; 8000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--subnet-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SUBNET_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--env&lt;/span&gt; &lt;span class="s2"&gt;"NEBIUS_API_KEY=&lt;/span&gt;&lt;span class="nv"&gt;$NEBIUS_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--env&lt;/span&gt; &lt;span class="s2"&gt;"MODEL_ID=meta-llama/Llama-3.3-70B-Instruct"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A CPU preset is enough here. The Endpoint doesn't run any model locally and only orchestrates calls out to Token Factory, so there's no benefit in paying for a GPU. &lt;code&gt;cpu-e2&lt;/code&gt; / &lt;code&gt;2vcpu-8gb&lt;/code&gt; is simply what this workload needs. You can run &lt;code&gt;nebius ai endpoint create --help&lt;/code&gt; to see the full range of platforms and presets if you want to size it differently.&lt;/p&gt;

&lt;p&gt;After two to three minutes, the endpoint moves to &lt;code&gt;Running&lt;/code&gt; and prints a public address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nebius ai endpoint get-by-name &lt;span class="nt"&gt;--name&lt;/span&gt; swimcoach-api &lt;span class="nt"&gt;--format&lt;/span&gt; json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://&amp;lt;public-ip&amp;gt;:8000/health
&lt;span class="c"&gt;# {"status": "ok"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Build the knowledge base as a Serverless Job
&lt;/h2&gt;

&lt;p&gt;A language model will happily produce a plausible-sounding training volume instead of a correct one. So, before planning agents are asked to produce anything, they need a source of real coaching knowledge to ground their decisions.&lt;/p&gt;

&lt;p&gt;This is where we'll use the second Nebius primitive. A &lt;strong&gt;Serverless Job&lt;/strong&gt; runs once, offline, and is well-suited to a task like this because there's no need to keep it running afterward. Once it's done, the compute is released automatically. The job loads twelve hand-written coaching profiles (one per combination of swimmer level and training goal) from &lt;code&gt;job/seed_data/training_principles.json&lt;/code&gt; (already included in the repo) and for each one makes a single call to Token Factory to compress the structured data into a short coaching summary suitable for prompt injection. The result is written to &lt;code&gt;knowledge_base.json&lt;/code&gt; and uploaded to Nebius Object Storage.&lt;/p&gt;

&lt;p&gt;The seed data is the part that actually determines whether the grounding means anything. training_principles.json was drafted to reflect widely published, publicly available swim coaching methodology documented in places like USMS's free training resources and British Swimming/Swim England's published coach education materials.&lt;/p&gt;

&lt;p&gt;The Job needs its own credentials for Object Storage. A static access key is tied to a service account, so create one of each first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PARENT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;your-project-id&amp;gt; &lt;span class="c"&gt;# find it via `nebius profile list`, then check the&lt;/span&gt;
                            &lt;span class="c"&gt;# parent-id field for that profile in ~/.nebius/config.yaml&lt;/span&gt;

nebius iam service-account create &lt;span class="nt"&gt;--name&lt;/span&gt; swimcoach-storage-sa &lt;span class="nt"&gt;--parent-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PARENT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# → note the service account id from the output&lt;/span&gt;

nebius iam v2 access-key create &lt;span class="nt"&gt;--account-service-account-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SA_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--parent-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PARENT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# → aws_access_key_id is S3_ACCESS_KEY, secret is S3_SECRET_KEY&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With those in hand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;job
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;NEBIUS_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;your-tokenfactory-key&amp;gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;S3_ACCESS_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;from-the-access-key-output-above&amp;gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;S3_SECRET_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;from-the-access-key-output-above&amp;gt;

docker buildx build &lt;span class="nt"&gt;--platform&lt;/span&gt; linux/amd64 &lt;span class="nt"&gt;-f&lt;/span&gt; Dockerfile.job &lt;span class="nt"&gt;-t&lt;/span&gt; docker.io/&amp;lt;your-username&amp;gt;/swimcoach-job:latest &lt;span class="nt"&gt;--push&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nebius ai job create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; swimcoach-data-processor &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt; docker.io/&amp;lt;your-username&amp;gt;/swimcoach-job:latest &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--platform&lt;/span&gt; cpu-d3 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--preset&lt;/span&gt; 4vcpu-16gb &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--env&lt;/span&gt; &lt;span class="s2"&gt;"NEBIUS_API_KEY=&lt;/span&gt;&lt;span class="nv"&gt;$NEBIUS_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--env&lt;/span&gt; &lt;span class="s2"&gt;"S3_ACCESS_KEY=&lt;/span&gt;&lt;span class="nv"&gt;$S3_ACCESS_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--env&lt;/span&gt; &lt;span class="s2"&gt;"S3_SECRET_KEY=&lt;/span&gt;&lt;span class="nv"&gt;$S3_SECRET_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--env&lt;/span&gt; &lt;span class="s2"&gt;"S3_BUCKET=swim-program"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check on it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nebius ai job logs &amp;lt;job-id&amp;gt; &lt;span class="nt"&gt;--follow&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A successful run enriches all twelve profiles and ends with &lt;code&gt;Uploaded to s3://swim-program/knowledge_base.json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: The Object Storage permission wall
&lt;/h2&gt;

&lt;p&gt;Nebius Object Storage doesn't grant a service account access to a bucket simply because that service account belongs to the same project. Access has to be explicitly granted through a bucket policy rule, and that rule can only be scoped to a group, not directly to an individual service account. So the fix is a short sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Work out which service account the storage key belongs to&lt;/span&gt;
nebius iam v2 access-key list &lt;span class="nt"&gt;--parent-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PARENT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; json
&lt;span class="c"&gt;# match on status.aws_access_key_id, read spec.account.service_account.id&lt;/span&gt;

&lt;span class="c"&gt;# 2. Put that service account into a group&lt;/span&gt;
nebius iam group create &lt;span class="nt"&gt;--name&lt;/span&gt; swim-program-writers &lt;span class="nt"&gt;--parent-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PARENT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
nebius iam group-membership create &lt;span class="nt"&gt;--parent-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GROUP_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--member-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SERVICE_ACCOUNT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# 3. Grant the group write access on the bucket&lt;/span&gt;
nebius storage bucket update &lt;span class="nt"&gt;--id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BUCKET_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bucket-policy-rules&lt;/span&gt; &lt;span class="s1"&gt;'[{"group_id": "'&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GROUP_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s1"&gt;'", "paths": ["*"], "roles": ["storage.editor"]}]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I folded the whole sequence into the deployment script &lt;code&gt;deploy_job.sh&lt;/code&gt;, which is written to check first and only create what's missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Wiring the knowledge base into the prompts
&lt;/h2&gt;

&lt;p&gt;The Endpoint doesn't read the knowledge base from Object Storage on every request. That would add an unnecessary network call to every&lt;code&gt;/generate-plan&lt;/code&gt; request for data that rarely changes. Instead, it fetches &lt;code&gt;knowledge_base.json&lt;/code&gt; once per container lifetime, on first use after the container starts, and keeps it in memory for as long as that process runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;_knowledge_base&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_knowledge_base&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;_knowledge_base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_fetch_from_object_storage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_knowledge_base&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_LOCAL_KB_PATH&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;_knowledge_base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_knowledge_base&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If Object Storage credentials aren't configured, or the fetch fails for any reason, it falls back to a bundled copy committed in the repo, so local development with no S3 credentials at all still works, and a brief Object Storage outage doesn't take the Endpoint down with it.&lt;/p&gt;

&lt;p&gt;This means refreshing the knowledge base after the Job runs doesn't need a rebuild or a new deployment. It needs the container to restart so it re-fetches on the next startup. Nebius Endpoints support exactly that with &lt;code&gt;stop&lt;/code&gt; and &lt;code&gt;start&lt;/code&gt;, separate from &lt;code&gt;delete&lt;/code&gt;/&lt;code&gt;create&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nebius ai endpoint stop &amp;lt;endpoint-id&amp;gt;
nebius ai endpoint start &amp;lt;endpoint-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;The Job is CPU-only and finishes in one to three minutes. The Endpoint, also CPU-only, runs five or six sequential calls to Token Factory per &lt;code&gt;/generate-plan&lt;/code&gt; request: one for periodization, one per week, and one batch call for coaching notes, so the total latency scales with the number of requested weeks. Measured end-to-end, a four-week plan takes roughly three minutes; a two-week plan takes about half that. That's a deliberate trade-off: each step's output is small and easy to validate on its own, at the cost of a request that takes minutes rather than seconds. &lt;/p&gt;

&lt;p&gt;A small static frontend with plain HTML, CSS, and JavaScript turns the JSON response into a plan that reads the way a swimmer would actually read a workout on a whiteboard: warmup, main set, and cooldown, each set written out as reps, distance, and rest.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn7qcwkslmt9lnkmc17h1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn7qcwkslmt9lnkmc17h1.png" width="799" height="692"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Generating a four-week plan makes seven Token Factory calls totalling roughly 9,000 tokens, at Llama 3.3 70B's rate of $0.13/1M input and $0.40/1M output. It’s about a quarter of a cent per plan. The Endpoint container is the main cost driver. It runs continuously regardless of traffic. Checked against Nebius's own cost calculator (&lt;code&gt;nebius billing v1alpha1 calculator estimate&lt;/code&gt;), the smallest non-GPU preset (&lt;code&gt;cpu-e2&lt;/code&gt; / &lt;code&gt;2vcpu-8gb&lt;/code&gt;) comes out to roughly $1.19/day in compute.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;#NebiusServerlessChallenge&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>nebiusserverlesschallenge</category>
      <category>fastapi</category>
    </item>
    <item>
      <title>Search and Replace: Getting Started with the Notion API SDK for JavaScript</title>
      <dc:creator>Andrei Parfenov</dc:creator>
      <pubDate>Thu, 29 Dec 2022 09:31:56 +0000</pubDate>
      <link>https://dev.to/andreiparfenov/search-and-replace-getting-started-with-the-notion-api-sdk-for-javascript-47j2</link>
      <guid>https://dev.to/andreiparfenov/search-and-replace-getting-started-with-the-notion-api-sdk-for-javascript-47j2</guid>
      <description>&lt;p&gt;Notion is a powerful tool for creating and managing a variety of digital content, such as notes, articles, and databases. However, one feature that many users have requested for a long time is the ability to search and replace text within Notion documents and pages.&lt;/p&gt;

&lt;p&gt;I have a lot of documents in my Notion databases and often need to search through all of the pages and replace specific words and phrases. &lt;/p&gt;

&lt;p&gt;For example, I have a table with instructions on how to work with remote repositories, and I want to replace all instances of &lt;strong&gt;"GitHub"&lt;/strong&gt; with &lt;strong&gt;"GitLab"&lt;/strong&gt; in every document:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fttgxr4oikswzo94sghs6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fttgxr4oikswzo94sghs6.png" alt="Notion database" width="800" height="248"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this post, I'll show you how to create a simple workaround to search for text and replace it in multiple documents inside a Notion database. All you need is Node.js installed on your machine.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Link to a GitHub repo with the full code: &lt;a href="https://github.com/andreiparfenov/search-and-replace-notion-api" rel="noopener noreferrer"&gt;https://github.com/andreiparfenov/search-and-replace-notion-api&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notion API Reference: &lt;a href="https://developers.notion.com/reference/intro" rel="noopener noreferrer"&gt;https://developers.notion.com/reference/intro&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is a very simple workaround where I mostly tried to experiment with the Notion API. &lt;strong&gt;Please note that I was only able to implement a solution for Notion text blocks or paragraphs.&lt;/strong&gt; My script will not work for headings, code snippets, etc. because different block types have different object properties in the Notion API. The code is also far from perfect, so feel free to leave your suggestions for improvement. Also, share about your experience working with the Notion API.&lt;/p&gt;

&lt;p&gt;Here's a step-by-step instruction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set up Notion
&lt;/h2&gt;

&lt;p&gt;Assuming that you have an account on Notion, go to &lt;a href="https://www.notion.so/my-integrations" rel="noopener noreferrer"&gt;the "My integrations" page&lt;/a&gt;. Click the "New integration" button. Create an internal integration with the ability to read, update, and insert content:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flmogghs9248x5nt0nun0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flmogghs9248x5nt0nun0.png" alt="Create a Notion integration" width="800" height="565"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Go to the database you want to work with, press "Settings" (three dots), and then click "Add connections". Choose the newly created integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set up the Node.js app
&lt;/h2&gt;

&lt;p&gt;Notion has a JavaScript client for the API. We'll create a folder for the new Node.js project and initialize &lt;code&gt;package.json&lt;/code&gt; inside:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir notion-search-and-replace
npm init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that we have the &lt;code&gt;package.json&lt;/code&gt; file, let's install a package with the Notion SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install @notionhq/client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a Node.js file (I created &lt;code&gt;app.js&lt;/code&gt;) in the project folder and initialize the Notion client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Client&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@notionhq/client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;notion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NOTION_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;NOTION_TOKEN&lt;/code&gt; is your internal integration token from Notion. If you share your code with others, make sure that it's kept in &lt;code&gt;.env&lt;/code&gt; as well.&lt;/p&gt;

&lt;p&gt;Next, specify the value that the script will search for and what it should be replaced with. I want to replace all occurrences of &lt;strong&gt;"GitHub"&lt;/strong&gt; with &lt;strong&gt;"GitLab"&lt;/strong&gt; in my text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;searchValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GitHub&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GitLab&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create an entry point function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;notion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;databases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;database_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NOTION_API_DATABASE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// we'll access the page's blocks here and update them&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this function, we access the database by its ID and create a loop to iterate over the pages (the &lt;code&gt;results&lt;/code&gt; property) inside the database.&lt;/p&gt;

&lt;p&gt;When we access each page from the database, we'll need to access the page's blocks. Let's do it in a separate function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;retrieveBlocks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pageId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;notion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;block_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pageId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Notion, a page is also a block. That's why in the code above we access the &lt;code&gt;children&lt;/code&gt; property of &lt;code&gt;blocks&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We'll need to go one more level down and access the text inside each block. Let's add the following function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;findAndReplace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paragraph&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;textItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paragraph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rich_text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newTextItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;textItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;selectedText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;selectedText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;selectedText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;selectedText&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;newText&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="c1"&gt;// we'll update the corresponding block here&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this function, we use the &lt;code&gt;paragraph.rich_text&lt;/code&gt; property, which is an array of objects. Yes, the text content is actually an array where elements are split by their formatting style. It may sound complicated, but here's an example:&lt;/p&gt;

&lt;p&gt;I &lt;strong&gt;love&lt;/strong&gt; &lt;code&gt;GitHub&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If this is a Notion paragraph, it will have four elements in the &lt;code&gt;rich_text&lt;/code&gt; array:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;"I" - non-formatted&lt;/li&gt;
&lt;li&gt;"love" - bold&lt;/li&gt;
&lt;li&gt;"GitHub" - code&lt;/li&gt;
&lt;li&gt;"." - non-formatted&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Read more about &lt;code&gt;rich-text&lt;/code&gt; in &lt;a href="https://developers.notion.com/reference/rich-text" rel="noopener noreferrer"&gt;the Notion API's docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Then we use &lt;code&gt;map()&lt;/code&gt; to replace every needed instance.&lt;/p&gt;

&lt;p&gt;Finally, let's update the entire block and insert the new text items:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newTextItems&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;selectedBlock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;notion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;block_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;paragraph&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;rich_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newTextItems&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update the &lt;code&gt;findAndReplace()&lt;/code&gt; function by calling &lt;code&gt;updateBlock()&lt;/code&gt; at the very end of the &lt;code&gt;for...of&lt;/code&gt; loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;findAndReplace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// all the previous code&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;updateBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newTextItems&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also, update the entry point function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;notion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;databases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;database_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NOTION_API_DATABASE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// call functions retrieveBlocks and findAndReplace here&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blocks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;retrieveBlocks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;findAndReplace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the code with the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node app.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script will replace all instances of &lt;strong&gt;"GitHub"&lt;/strong&gt; in all the documents inside the database:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqa60nz2jsq599kq3jnuy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqa60nz2jsq599kq3jnuy.png" alt="Page before running the script" width="800" height="491"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhdzc0eoyptaxs4byqh5u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhdzc0eoyptaxs4byqh5u.png" alt="Page after running the screen" width="799" height="510"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I hope you find this useful. Share how you use the Notion API to automate your work. 🤖&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>python</category>
      <category>codenewbie</category>
      <category>community</category>
    </item>
  </channel>
</rss>
