<?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: Benson King'ori</title>
    <description>The latest articles on DEV Community by Benson King'ori (@virgoalpha).</description>
    <link>https://dev.to/virgoalpha</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1142344%2Fef0f07a9-d21c-4b26-bd16-9a90053c92a3.jpeg</url>
      <title>DEV Community: Benson King'ori</title>
      <link>https://dev.to/virgoalpha</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/virgoalpha"/>
    <language>en</language>
    <item>
      <title>Resurrecting Google Reader for the modern web using Kiro</title>
      <dc:creator>Benson King'ori</dc:creator>
      <pubDate>Sat, 20 Dec 2025 14:13:09 +0000</pubDate>
      <link>https://dev.to/aws-builders/resurrecting-google-reader-for-the-modern-web-using-kiro-201k</link>
      <guid>https://dev.to/aws-builders/resurrecting-google-reader-for-the-modern-web-using-kiro-201k</guid>
      <description>&lt;h1&gt;
  
  
  TLDR;
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Google Reader solved content tracking for the open web, but died when RSS could not keep up with SPAs.&lt;/li&gt;
&lt;li&gt;Watcher resurrects the Google Reader experience for the modern web.&lt;/li&gt;
&lt;li&gt;Instead of relying on RSS, Watcher creates RSS by monitoring live webpages.&lt;/li&gt;
&lt;li&gt;Users define “haunts” using natural language; AI generates selectors and structure.&lt;/li&gt;
&lt;li&gt;The UI intentionally mirrors Google Reader’s three-column layout and power-user workflow.&lt;/li&gt;
&lt;li&gt;AI + scraping + RSS unlocks a new class of user-controlled web monitoring tools.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;li&gt;
Kiro and its features

&lt;ul&gt;
&lt;li&gt;Spec driven development&lt;/li&gt;
&lt;li&gt;Vibe coding&lt;/li&gt;
&lt;li&gt;Agent hooks&lt;/li&gt;
&lt;li&gt;Steering docs&lt;/li&gt;
&lt;li&gt;MCP&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Google Reader&lt;/li&gt;

&lt;li&gt;

Watcher

&lt;ul&gt;
&lt;li&gt;How we built it&lt;/li&gt;
&lt;li&gt;Similarities Between Watcher and Google Reader&lt;/li&gt;
&lt;li&gt;Differences Between Watcher and Google Reader&lt;/li&gt;
&lt;li&gt;Lessons learnt&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Conclusion&lt;/li&gt;

&lt;/ul&gt;

&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;I was honored to participate in the Kiroween Hackathon on Devpost, an event that challenged participants to build ambitious projects using Kiro, AWS’s newly released AI-native IDE. The hackathon encouraged not just technical execution, but creative re-thinking across four themes: resurrecting dead technologies, stitching together unlikely systems, building flexible foundations, or delivering unforgettable interfaces.&lt;/p&gt;

&lt;p&gt;For my submission, I chose &lt;strong&gt;Resurrection&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Over a decade ago, Google Reader quietly disappeared from the web. Its shutdown marked more than the loss of a product, it signaled a shift away from user-controlled, open content consumption toward algorithmically curated feeds. Yet the problem Reader solved never went away. In fact, it became harder: the modern web moved to dynamic, JavaScript-heavy applications that no longer expose RSS at all.&lt;/p&gt;

&lt;p&gt;This project explores a simple question: What would Google Reader look like if it were rebuilt for today’s web?&lt;/p&gt;

&lt;p&gt;The result is Watcher, a system that haunts modern websites, detects meaningful change, and resurrects the RSS model using AI, scraping, and a deliberately nostalgic interface. Kiro made this possible.&lt;/p&gt;

&lt;h1&gt;
  
  
  Kiro and its features
&lt;/h1&gt;

&lt;p&gt;Kiro is an IDE that AWS released this year. It has several cool features such as:&lt;/p&gt;

&lt;h2&gt;
  
  
  Spec driven development
&lt;/h2&gt;

&lt;p&gt;Spec-driven development in Kiro places formal specifications at the center of the workflow. Instead of writing code first and documenting later, developers define structured specs that describe intent, constraints, and expected behavior. These specs are then used by the IDE and its agents to guide implementation, validation, and refactoring. This approach reduces ambiguity, improves alignment between stakeholders, and creates a durable source of truth that evolves alongside the codebase. For AI-assisted development, specs act as guardrails, ensuring that generated code remains consistent with the system’s design goals.&lt;/p&gt;

&lt;p&gt;To use this mode, I first wrote the PRD (Product Requirements Document) by hand and added it as a spec in Kiro. Kiro then used it to generate the requirements, design and tasks file. I then executed the tasks one by one and checked to ensure that the code generated met my standards and creative vision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vibe coding
&lt;/h2&gt;

&lt;p&gt;Vibe coding is Kiro’s term for an exploratory, conversational style of development where developers work at the level of intent rather than syntax. Instead of issuing narrowly scoped prompts, developers express what they are trying to achieve,architecturally or experientially, and allow the IDE to propose implementations that fit the broader context of the project. This mode is particularly effective in early-stage prototyping, where requirements are fluid and rapid iteration is essential. Vibe coding prioritizes flow and momentum while still grounding outputs in the project’s specifications and constraints.&lt;/p&gt;

&lt;p&gt;I used vie coding to debug and refine the UI components to my needs. It proved useful in understanding the code generated as well as the cause of errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agent hooks
&lt;/h2&gt;

&lt;p&gt;Agent hooks allow developers to attach AI agents to specific lifecycle events such as file changes, test failures, or deployment steps. These agents can observe state, reason about deltas, and take targeted actions,ranging from suggesting fixes to generating artifacts or alerts. Rather than operating as a monolithic assistant, Kiro’s agents are modular and event-driven, which makes them predictable and composable. This model mirrors how modern systems are built: loosely coupled components reacting to well-defined signals.&lt;/p&gt;

&lt;p&gt;I created agent hooks for security, performance and unit testing goals. These ensured that I had the basics covered as I continued to iteratively develop my project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Steering docs
&lt;/h2&gt;

&lt;p&gt;Steering documents in Kiro are lightweight, high-leverage artifacts that encode architectural principles, design philosophies, and non-functional requirements. They serve as long-lived guidance for both humans and AI agents, shaping decisions without prescribing implementation details. In practice, steering docs help maintain coherence as a project grows, especially when multiple contributors or agents are involved. They are particularly valuable in AI-assisted environments, where consistent direction is necessary to avoid fragmentation and unintended complexity.&lt;/p&gt;

&lt;p&gt;I used steering docs to set guardrails for the design and set up. I wanted to try and mimic Google Reader’s UI and functionality as much as possible and this came in handy.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP
&lt;/h2&gt;

&lt;p&gt;The Model Context Protocol (MCP) provides a standardized way to supply structured context,such as schemas, APIs, domain models, and external tools,to AI agents. By formalizing how context is shared, MCP reduces hallucinations and increases the reliability of agent outputs. It enables agents to operate with a clear understanding of the system’s boundaries and available capabilities, making them more effective collaborators rather than generic text generators. MCP is a critical enabler for building production-grade, AI-native developer workflows.&lt;/p&gt;

&lt;h1&gt;
  
  
  Google Reader
&lt;/h1&gt;

&lt;p&gt;Google Reader was a web-based RSS and Atom feed aggregator launched by Google in 2005. At its core, it allowed users to subscribe to content feeds,blogs, news sites, academic journals, forums,and consume updates in a single, unified interface. Rather than visiting dozens of websites individually, users could rely on Google Reader to surface new content as it was published, ordered chronologically and optimized for rapid scanning. Its minimal, text-first interface emphasized efficiency over distraction, enabling power users to process large volumes of information quickly.&lt;/p&gt;

&lt;p&gt;Google Reader was important because it embodied an open, decentralized model of the web. It rewarded publishers who exposed structured feeds and gave users direct control over how and where they consumed information, independent of proprietary algorithms. For researchers, journalists, developers, and analysts, it became an indispensable tool for monitoring changes across many sources. It also pioneered interaction patterns,such as keyboard shortcuts, starring, tagging, and sharing,that influenced later content consumption tools.&lt;/p&gt;

&lt;p&gt;Despite its loyal user base, Google shut down Reader in 2013, citing declining usage and a strategic shift toward fewer, more focused products. In practice, its closure reflected a broader industry transition away from open syndication toward algorithmically curated social feeds. While platforms like Twitter and Facebook offered scale and engagement, they replaced user intent with opaque ranking systems. The shutdown left a lasting gap for users who valued transparency, control, and signal over noise,a gap that many modern tools, including Watcher, aim to address.&lt;/p&gt;

&lt;h1&gt;
  
  
  Watcher
&lt;/h1&gt;

&lt;p&gt;Google Reader was one of the most beloved tools on the web: simple, fast, and incredibly efficient at keeping people updated. But as the web shifted to SPAs and dynamic content, most of it without RSS, Reader’s death left a real gap.&lt;/p&gt;

&lt;p&gt;Watcher was born from the idea: What if we resurrected Google Reader, but upgraded it to haunt the modern web? Meaning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It can watch any page, including SPAs&lt;/li&gt;
&lt;li&gt;It understands natural language&lt;/li&gt;
&lt;li&gt;It detects meaningful changes&lt;/li&gt;
&lt;li&gt;And it exposes everything again as RSS, just like the old days&lt;/li&gt;
&lt;li&gt;That mix of nostalgia + modern constraints was the spark.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Watcher resurrects the Google Reader experience for the modern web.&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%2Fhjq6259h0pacxg5cxpa9.gif" 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%2Fhjq6259h0pacxg5cxpa9.gif" alt="Watcher GIF" width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can view the deployed website &lt;a href="https://dclgubjbfzflp.cloudfront.net/welcome" rel="noopener noreferrer"&gt;here&lt;/a&gt;. Use the below credentials:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Email&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;demo@watcher.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Password&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;demo123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It lets users:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Define a haunt by giving a URL + natural language description like: “Tell me when the admissions page says applications are open for 2026.”&lt;/li&gt;
&lt;li&gt;Behind the scenes, an LLM generates selectors, keys, and normalization rules.&lt;/li&gt;
&lt;li&gt;A headless browser (Playwright) scrapes the target on a schedule.&lt;/li&gt;
&lt;li&gt;Watcher tracks key/value state diffs, not raw HTML, and generates structured change events.&lt;/li&gt;
&lt;li&gt;Each haunt produces an RSS feed.&lt;/li&gt;
&lt;li&gt;The UI is a faithful rebirth of the 3-column Google Reader layout, complete with folders, unread counts, stars, refresh, and keyboard shortcuts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short: Watcher turns any webpage, even SPAs, into a live RSS source.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we built it
&lt;/h2&gt;

&lt;p&gt;Watcher is built as a Django-based system with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spec-driven functional requirements covering scraping, diffing, RSS construction, and a Reader-style UI.&lt;/li&gt;
&lt;li&gt;Playwright for SPA rendering and key extraction.&lt;/li&gt;
&lt;li&gt;Celery for periodic haunting and change detection.&lt;/li&gt;
&lt;li&gt;A fully modeled haunt configuration, derived via LLM from natural language.&lt;/li&gt;
&lt;li&gt;Structured state tracking, storing only key/value diffs and summaries.&lt;/li&gt;
&lt;li&gt;RSS feed generation for both private and public haunts.&lt;/li&gt;
&lt;li&gt;A Google Reader–inspired front-end, implemented to feel as close as possible to the original.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kiro powered the development loop, particularly around specs, architecture constraints, steering for UI generation, and consistency between backend and frontend layers.&lt;/p&gt;

&lt;p&gt;You can find the code base on &lt;a href="https://github.com/Virgo-Alpha/Watcher" rel="noopener noreferrer"&gt;github&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Similarities Between Watcher and Google Reader
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Feed-Oriented Information Consumption&lt;/strong&gt;
Both Watcher and Google Reader organize information in a feed-like format that lets users see updates from multiple sources in a unified view. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RSS Integration and Support&lt;/strong&gt;
Both systems can work with RSS sources: Google Reader was built around RSS/Atom feed aggregation, while Watcher supports adding RSS cybersecurity sources into its monitoring. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three-Panel Interface and Navigation&lt;/strong&gt;
Watcher’s interface intentionally draws on the three-panel layout that was characteristic of Google Reader, navigation pane, feed list, and content view. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unread/Read Tracking&lt;/strong&gt;
Both platforms include mechanisms to mark items as read or unread, enabling users to track what they have and have not seen. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyboard Shortcuts and Power User Features&lt;/strong&gt;
Google Reader popularized keyboard shortcuts (J/K/M/S) and Watcher includes similar navigation controls inspired by Reader. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subscription Model for Content&lt;/strong&gt;
Google Reader let users subscribe to feeds; Watcher lets users subscribe to monitoring configurations (“haunts”) and view updates similarly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Differences Between Watcher and Google Reader
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Google Reader&lt;/th&gt;
&lt;th&gt;Watcher&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Primary Purpose&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;General-purpose RSS/Atom feed aggregator for web content and news.&lt;/td&gt;
&lt;td&gt;Website change monitoring and alerting with AI-assisted context.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Core Functionality&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Aggregates syndicated feeds and surfaces updates for reading.&lt;/td&gt;
&lt;td&gt;Continuously monitors pages (including SPAs) and detects meaningful changes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AI Integration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None; designed as a human-driven feed reader.&lt;/td&gt;
&lt;td&gt;Uses AI to interpret change relevance and generate selectors from natural language.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Update Detection Mechanism&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pulls standardized feed entries as published by websites.&lt;/td&gt;
&lt;td&gt;Uses headless browsers (e.g., Playwright) to detect changes beyond RSS.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Notification Types&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;In-app unread counts and keyword search; limited alerts.&lt;/td&gt;
&lt;td&gt;Email alerts and structured summaries when defined conditions trigger.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;User Interaction Model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Users subscribe to feeds and consume published entries.&lt;/td&gt;
&lt;td&gt;Users define what to monitor (“haunts”); the system proactively watches for changes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Social Features&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Experimental sharing features (later removed).&lt;/td&gt;
&lt;td&gt;Public haunts and subscriptions to other users’ monitoring configurations.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scope of Content&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Limited to content explicitly exposed via RSS/Atom.&lt;/td&gt;
&lt;td&gt;Can monitor arbitrary webpages, including dynamic and JavaScript-rendered content.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Historical Status&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Discontinued in 2013.&lt;/td&gt;
&lt;td&gt;Actively developed and deployable.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Lessons learnt
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;LLMs work exceptionally well when guided by tight specs + steering documents.&lt;/li&gt;
&lt;li&gt;The web’s move to SPAs made RSS impossible, but not undetectable.&lt;/li&gt;
&lt;li&gt;State diffs matter more than raw HTML when building meaningful alerts.&lt;/li&gt;
&lt;li&gt;Nostalgia is a powerful design force, porting old UX patterns into modern stacks teaches discipline.&lt;/li&gt;
&lt;li&gt;Combining AI + scraping + RSS can create genuinely new value.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;Watcher began as an exercise in nostalgia, but it ended as a statement about the modern web. While RSS disappeared not because it was flawed, but because the web outgrew it, the underlying need, to know when something meaningfully changes, never went away.&lt;/p&gt;

&lt;p&gt;By combining AI-driven interpretation, structured state diffing, and headless browser scraping, Watcher turns even the most dynamic SPA into a first-class, queryable feed. In doing so, it restores user intent, transparency, and control, values that defined tools like Google Reader but are largely absent today.&lt;/p&gt;

&lt;p&gt;Kiro proved to be more than an IDE in this process. Its emphasis on specs, steering documents, and agent-driven workflows enabled a level of architectural consistency that would have been difficult to maintain in an AI-assisted build. Rather than fighting the model, the system was shaped by constraints.&lt;/p&gt;

&lt;p&gt;The broader lesson is this: AI does not replace structure, it amplifies it. When paired with clear specs, thoughtful design, and a respect for proven UX patterns, it enables entirely new classes of systems.&lt;/p&gt;

&lt;p&gt;Watcher is one such system. A resurrection, not of a product, but of an idea: that the web should work for its users, not the other way around.&lt;/p&gt;

</description>
      <category>kiroween</category>
      <category>webdev</category>
      <category>programming</category>
      <category>kiro</category>
    </item>
    <item>
      <title>AWS Cloud Resume Challenge - my attempt</title>
      <dc:creator>Benson King'ori</dc:creator>
      <pubDate>Tue, 02 Dec 2025 10:25:13 +0000</pubDate>
      <link>https://dev.to/aws-builders/aws-cloud-resume-challenge-my-attempt-544m</link>
      <guid>https://dev.to/aws-builders/aws-cloud-resume-challenge-my-attempt-544m</guid>
      <description>&lt;h1&gt;
  
  
  TLDR;
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;I built and deployed an AWS Cloud Resume Challenge project: a secure, static resume site with a live visitor counter.&lt;/li&gt;
&lt;li&gt;Frontend: HTML/CSS/JS hosted in S3 and served globally via CloudFront with HTTPS and a private S3 origin (OAI).&lt;/li&gt;
&lt;li&gt;Backend: API Gateway → Lambda → DynamoDB to increment and return the visitor count, then display it on the page.&lt;/li&gt;
&lt;li&gt;IaC + CI/CD: Provisioned resources with AWS SAM (CloudFormation under the hood) and automated deployments/testing with GitHub Actions.&lt;/li&gt;
&lt;li&gt;Production-minded extras: Added CloudWatch logs, metrics, alarms, a dashboard, and SNS email notifications for monitoring and alerting.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Table Of Contents
&lt;/h1&gt;

&lt;p&gt;Introduction&lt;br&gt;
The Problem&lt;br&gt;
My Design&lt;br&gt;
Implementation&lt;br&gt;
Issues encountered&lt;br&gt;
Lessons learnt&lt;br&gt;
Future Work&lt;br&gt;
Conclusion&lt;/p&gt;

&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;The Cloud resume challenge is a great introduction to cloud concepts and gives you a detailed specification of what to build. Even for experienced builders, this challenge offers a refresher on the basic principles of the cloud such as databases and serverless architecture. You can find out more information about the aws version of the challenge &lt;a href="https://cloudresumechallenge.dev/docs/the-challenge/aws/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Personally, I decided to attempt the challenge for three main reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I am AWS Certified twice, which gives me a head start. You see, the first step of the challenge is certification and the last step is a blog post.&lt;/li&gt;
&lt;li&gt;I already have AWS credits that I could utilize. I got these as part of the benefits of being an AWS Cloud Builder. This was also how I got the vouchers to get certified in the first place. If you’re interested to learn more, read up about the program and how to join the upcoming cohort please have a look at the guide &lt;a href="https://builder.aws.com/content/35c9eBOBVhurX9sjp3YEyMtbOxU/future-aws-community-builder-step-by-step-guide" rel="noopener noreferrer"&gt;here&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;I wanted to learn new technologies that I had not interacted with before such as SAM (Serverless Application Model).&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  The Problem
&lt;/h1&gt;

&lt;p&gt;The Problem Statement of the Cloud Resume Challenge, if I was to use my own words, is to host your resume on the cloud, keep a record of the number of visitors on the exposed page and expose this number on your Front end. It also encourages use of Infrastructure as Code and CICD Principles. For the AWS Challenge, the specification include the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Certification&lt;/li&gt;
&lt;li&gt; HTML&lt;/li&gt;
&lt;li&gt; CSS&lt;/li&gt;
&lt;li&gt; Static Website&lt;/li&gt;
&lt;li&gt; HTTPS&lt;/li&gt;
&lt;li&gt; DNS (no custom domain yet – using CloudFront URL)&lt;/li&gt;
&lt;li&gt; JavaScript&lt;/li&gt;
&lt;li&gt; Database (DynamoDB)&lt;/li&gt;
&lt;li&gt; API (API Gateway + Lambda)&lt;/li&gt;
&lt;li&gt; Python&lt;/li&gt;
&lt;li&gt; Tests &lt;/li&gt;
&lt;li&gt; Infrastructure as Code (AWS SAM)&lt;/li&gt;
&lt;li&gt; Source Control (GitHub)&lt;/li&gt;
&lt;li&gt; CI/CD (Backend)&lt;/li&gt;
&lt;li&gt; CI/CD (Frontend)&lt;/li&gt;
&lt;li&gt; Blog Post&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Spoiler alert: I did not complete all of the  steps, but I will expound on that later in this blog post.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  My Design
&lt;/h1&gt;

&lt;p&gt;I made a simple design that included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The infrastructure defined on a SAM template&lt;/li&gt;
&lt;li&gt;The Frontend consisting of the HTML, CSS and JS hosted in an s3 bucket as needed, as well as cloudfront&lt;/li&gt;
&lt;li&gt;The Backend consisting of my lambda, database and API Gateway&lt;/li&gt;
&lt;/ul&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%2Frdy2z1n3myocl4vwqu2z.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%2Frdy2z1n3myocl4vwqu2z.png" alt="Image of my design" width="634" height="626"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I also extended the design to add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cloudwatch metrics and alerts&lt;/li&gt;
&lt;li&gt;SNS topic to send me emails of the cloudwatch alerts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I decided not to implement the custom domain name using Route 53 because of the costs. So far everything else would cost me nothing because of my cloud credits. However, the credits do not cover the purchase of a domain.&lt;/p&gt;

&lt;h1&gt;
  
  
  Implementation
&lt;/h1&gt;

&lt;p&gt;Regardless of the order of the specs, I needed to find a way to iteratively build that works for me. I started with the Frontend and tested it locally first. I then deployed all the resources I needed (for the Frontend) using the SAM template. I then implemented the lambda, API Gateway and Dynamodb for the backend. I tested the lambda locally using unit tests and the API Gateway (deployed) using integration tests. Only then did I add monitoring via cloudwatch logs, alerts and a dashboard. I then added an SNS topic to receive the alerts from cloudwatch.&lt;/p&gt;

&lt;p&gt;As part of the extension, I added dark mode on the front end and a button to download my resume as a pdf. Initially, this was just using the browsers &lt;code&gt;print to pdf&lt;/code&gt; functionality but the resume was turning out as two pages. Eventually, I just made the button to open up to a pdf version hosted online which one can download.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security
&lt;/h2&gt;

&lt;p&gt;Security is a major issue to consider when designing any system. I addressed it in the following ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I prevented direct access to the site using the s3 bucket link. I implemented Origin Access Identity (OAI) so that only CloudFront is allowed to fetch objects from the bucket&lt;/li&gt;
&lt;li&gt;Cloufront redirects HTTP to HTTPS. This ensures that all data that is in transit is encrypted.&lt;/li&gt;
&lt;li&gt;I implemented least privilege permissions in IAM. For example, Lambda can only make calls to the dynamodb and cloudwatch metrics&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Costs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What typically stays free / very low-cost for this project
&lt;/h3&gt;

&lt;p&gt;Within normal personal-portfolio traffic, the costs tend to be near-zero because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;S3 storage&lt;/strong&gt; is tiny (a few files)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudFront&lt;/strong&gt; has a generous free tier (and static content is cheap)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda free tier&lt;/strong&gt; includes lots of requests and compute time&lt;/li&gt;
&lt;li&gt;DynamoDB on-demand for a single item updated occasionally is tiny&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudWatch&lt;/strong&gt; basic metrics are included; small log volume is cheap&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Where cost risks can appear
&lt;/h3&gt;

&lt;p&gt;Even “free-tier-friendly” setups can cost money if something spikes:&lt;br&gt;
&lt;strong&gt;1. CloudWatch Logs&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If your Lambda logs a lot (especially per request), logs can grow.&lt;/li&gt;
&lt;li&gt;Log ingestion and retention may incur costs.&lt;/li&gt;
&lt;li&gt;Mitigation: set log retention (e.g., 7–14 days) and avoid noisy logs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. API Gateway&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It charges per request.&lt;/li&gt;
&lt;li&gt;A traffic spike or bot traffic can increase costs.&lt;/li&gt;
&lt;li&gt;Mitigation: rate limiting (usage plans), WAF, or CloudFront in front of API (advanced).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Lambda invocations&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Still cheap, but if your API is hammered, invocations increase.&lt;/li&gt;
&lt;li&gt;Mitigation: caching, bot protection, or reducing how often the browser calls &lt;code&gt;/count&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. CloudFront data transfer&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you ever serve large files or heavy traffic, bandwidth is usually the cost driver.&lt;/li&gt;
&lt;li&gt;Mitigation: caching, compression, keep assets small.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CloudFront caching reduces origin traffic for static assets, which keeps performance high and costs low. This is because after the first request, many users are served from CloudFront edge locations instead of pulling from S3 every time.&lt;/p&gt;

&lt;p&gt;The visitor counter remains dynamic and triggers Lambda; we could reduce those calls by caching the API response or only calling it once per session.&lt;/p&gt;

&lt;h1&gt;
  
  
  Issues encountered
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Permissions &amp;amp; Least Privilege (IAM)
&lt;/h2&gt;

&lt;p&gt;One of the first issues I ran into was related to IAM permissions. I started with a strict least-privilege policy for the Lambda function: it could only update a single DynamoDB table. That worked fine until I expanded the project to include custom CloudWatch metrics (for things like page views).&lt;br&gt;
At that point, my integration tests began failing with HTTP 500 responses from the API. Unit tests still passed because they used mocked AWS services, but the deployed Lambda in AWS was failing at runtime. The root cause was that the Lambda role didn’t have permission to publish metrics to CloudWatch (cloudwatch:PutMetricData). Adding that permission fixed the 500s.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Strategy (Unit + Integration)
&lt;/h2&gt;

&lt;p&gt;Testing was another area where the implementation forced me to think more clearly about what I was validating.&lt;br&gt;
Unit tests ran fully offline using mocks (e.g., moto), allowing me to test the Lambda logic quickly and repeatedly.&lt;/p&gt;

&lt;p&gt;Integration tests hit the live API Gateway endpoint and verified that DynamoDB was actually updating. This was useful because it caught problems unit tests could never detect, such as missing permissions, incorrect region configuration, and miswired resources.&lt;/p&gt;

&lt;p&gt;To keep CI/CD efficient and reduce noise, I configured the pipeline so tests run only when relevant code changes. For example, backend tests trigger on changes in backend folders or infrastructure templates, while deployment and integration tests only run on the main branch. That approach keeps pull requests fast while still protecting the main branch with “real” end-to-end validation.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI/CD Failures Due to DynamoDB Region / AWS Region Configuration
&lt;/h2&gt;

&lt;p&gt;A particularly annoying CI issue came from region configuration. The Lambda and DynamoDB code relied on boto3’s default region discovery. Locally, I had a region configured, so everything seemed fine. But in CI/CD, boto3 sometimes didn’t resolve a region the way I expected, which caused failures like NoRegionError when the code tried to talk to DynamoDB.&lt;/p&gt;

&lt;p&gt;The fix was to be explicit: set the region consistently via environment variables and ensure boto3 clients/resources use it. It was a good lesson in writing cloud code that behaves the same in three environments: local development, GitHub Actions, and AWS Lambda. When something works locally but fails in CI, it’s often because local credentials or config is hiding assumptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  SNS Subscription Emails Going to Spam
&lt;/h2&gt;

&lt;p&gt;After setting up monitoring alerts, I added an SNS topic to notify my email address. The infrastructure deployed fine, but I initially missed the subscription confirmation email because it landed in my email’s spam folder. Since SNS won’t send alerts until the subscription is confirmed, this can silently break alerting.&lt;br&gt;
Once I confirmed the subscription and moved the email out of spam, notifications started working.&lt;/p&gt;

&lt;h1&gt;
  
  
  Lessons learnt
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;SAM templates still end up as standard CloudFormation resources. This means that you can inspect Cloudformation in the aws console when debugging&lt;/li&gt;
&lt;li&gt;Permissions and infrastructure management, just like code, is iterative.&lt;/li&gt;
&lt;li&gt;Observability and monitoring are a part of the non-functional requirements, they can help catch errors that tests won’t&lt;/li&gt;
&lt;li&gt;Cloudfront caching can help reduce costs and enhance performance for static assets&lt;/li&gt;
&lt;li&gt;Operational details still matter e.g., if a user does not confirm an sns subscription, it doesn’t matter how correct the infrastructure is.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Future Work
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Enable active tracing for the Lambda + API Gateway using AWS X-ray to visualize request paths, latency, and failures end-to-end.&lt;/li&gt;
&lt;li&gt;Attach a custom domain to CloudFront using Route 53 + ACM&lt;/li&gt;
&lt;li&gt;Improve visitor counting: cumulative, daily, or unique visitors&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;Overall, I found the challenge fun and engaging. I hope you decide to take it. Like me, you can take it for your reasons and you can even skip parts. Don’t let the lack of a certification or anything else stop you. You can view my deployed site &lt;a href="https://d1aafmx13pzuu5.cloudfront.net/" rel="noopener noreferrer"&gt;here&lt;/a&gt; and the github repository &lt;a href="https://github.com/Virgo-Alpha/cloud-resume-challenge" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>githubactions</category>
      <category>sam</category>
    </item>
    <item>
      <title>Building “Sentinel”: multi-agent cybersecurity news triage and publishing system on AWS</title>
      <dc:creator>Benson King'ori</dc:creator>
      <pubDate>Mon, 29 Sep 2025 11:36:16 +0000</pubDate>
      <link>https://dev.to/aws-builders/building-sentinel-multi-agent-cybersecurity-news-triage-and-publishing-system-on-aws-5h5h</link>
      <guid>https://dev.to/aws-builders/building-sentinel-multi-agent-cybersecurity-news-triage-and-publishing-system-on-aws-5h5h</guid>
      <description>&lt;h1&gt;
  
  
  TLDR;
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;I built &lt;strong&gt;Sentinel&lt;/strong&gt;, a serverless, agentic pipeline that turns noisy RSS feeds into &lt;strong&gt;actionable cybersecurity intel&lt;/strong&gt; (dedup, triage, publish).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic first, agentic later&lt;/strong&gt;: Step Functions → Lambdas, then flipped a feature flag to Bedrock AgentCore (Strands) without changing contracts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability by design&lt;/strong&gt;: SQS buffering, DLQs, idempotency keys, guardrails, and graceful degradation (semantic → heuristic).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search that scales&lt;/strong&gt;: OpenSearch Serverless with &lt;strong&gt;BM25 + vectors&lt;/strong&gt;, cached embeddings, clusters for near-dupes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure &amp;amp; observable&lt;/strong&gt;: Cognito + least privilege, KMS, WAF, VPC endpoints, JSON logs + X-Ray, SLOs &amp;amp; cost alarms.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Table Of Contents
&lt;/h1&gt;

&lt;p&gt;Introduction&lt;br&gt;
Problem statement&lt;br&gt;
My solution&lt;br&gt;
The architecture&lt;br&gt;
Use of Strands&lt;br&gt;
Use of Bedrock&lt;br&gt;
Human in the loop&lt;br&gt;
Challenges and breakthroughs&lt;br&gt;
Key learnings&lt;br&gt;
Future Plans&lt;br&gt;
Conclusion&lt;/p&gt;

&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;Exploring AWS AI offerings has been on my TODO list for the longest time. I was particularly interested in Strands, Bedrock and Nova Act. Thus, for this AI Engineering month, I decided to take on the challenge to solve a practical problem that I have seen in my industry using these tools, while learning and exploring in the process. I recently earned the AWS Certified Solutions Architect Associate certification and also got access to Kiro so this project allowed me to play the part of a technical PM and apply my system design skills. I hope you learn something that may aid you in your work. Enjoy.&lt;/p&gt;

&lt;h1&gt;
  
  
  Problem statement
&lt;/h1&gt;

&lt;p&gt;As my company’s CISO, I would like to develop an internal cybersecurity newsletter that collates news from different RSS feeds, filters out those relevant to my organization based on a list of keywords and shares them with fellow employees either via email or published on an internal site. I wanted to be kept abreast of the latest happenings in the industry but I want to automatically share anything that may be relevant so that’s why I expanded my requirements.&lt;/p&gt;

&lt;h1&gt;
  
  
  My solution
&lt;/h1&gt;

&lt;p&gt;Sentinel is an AWS-native, multi-agent cybersecurity news triage and publishing system that autonomously ingests, processes, and publishes cybersecurity intelligence from RSS feeds and news sources. The system reduces analyst workload by automatically deduplicating content, extracting relevant entities, and intelligently routing items for human review or auto-publication.&lt;br&gt;
For the full code, visit my repository &lt;a href="https://github.com/Virgo-Alpha/Sentinel" rel="noopener noreferrer"&gt;here&lt;/a&gt;&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%2Flxvykidmuherucqdqcso.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%2Flxvykidmuherucqdqcso.png" alt="Information flow" width="800" height="1006"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  The architecture
&lt;/h1&gt;

&lt;p&gt;I designed a &lt;strong&gt;decoupled, serverless microservices architecture&lt;/strong&gt; that scales cleanly and kept costs predictable. The core remained a set of Lambda functions behind Step Functions, with EventBridge schedules kicking off ingestion. I routed all content through a buffered pipeline: EventBridge fanned into SQS so bursts of feeds didn’t cascade into failures, and every consumer Lambda processed messages idempotently using a canonical-URL SHA-256 as the key. I attached DLQs at each hop (EventBridge, SQS consumers, Step Functions tasks) and wrote compensation paths so partial successes (e.g., stored raw content but failed dedup) re-queued safely.&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%2Fk6xu3t9ek6vkqorkkxc1.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%2Fk6xu3t9ek6vkqorkkxc1.png" alt="My architecture" width="800" height="658"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I modeled &lt;strong&gt;storage&lt;/strong&gt; deliberately. In DynamoDB I used a primary table for articles with GSIs for state#published_at (queues and dashboards), cluster_id#published_at (duplicates), and tags#published_at (topic browsing). I enabled TTL for short-term session memory and configured PITR for recovery. For search, I provisioned OpenSearch Serverless with a BM25 collection for keyword queries and a k-NN vector collection for semantic near-duplicate detection. I cached embeddings by content hash to avoid recomputation and cut latency.&lt;/p&gt;

&lt;p&gt;On the &lt;strong&gt;agent&lt;/strong&gt; side, I started with direct orchestration so I could validate the pipeline deterministically. Step Functions called Lambdas (FeedParser → Relevancy → Dedup → Summarize → Guardrail → Decision), and a thin “agent shim” Lambda exposed the same interface I knew I’d use later. When I was ready, I deployed my Strands-defined Ingestor and Analyst Assistant agents to Bedrock AgentCore and flipped a feature flag so Step Functions invoked the agents instead. If AgentCore became unavailable or too slow, the same flag let me fall back instantly to direct Lambda orchestration.&lt;/p&gt;

&lt;p&gt;I treated &lt;strong&gt;configuration&lt;/strong&gt; and behavior as data. I moved feed lists, keyword taxonomies, similarity thresholds, guardrail strictness, and rollout switches into SSM Parameter Store, and I read them at runtime. I kept a small but explicit flags matrix: enable_agents (direct vs AgentCore), enable_opensearch (heuristic vs semantic dedup), enable_amplify (backend-only vs full stack), enable_guardrails_strict, and enable_digest_email. This let me ship incrementally without redeploys.&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;Bedrock usage&lt;/strong&gt;, I separated concerns. I used LLM calls to score relevance to my taxonomy and extract entities (CVE IDs, threat actors, malware, vendors/products) with confidences and rationales. I generated two summaries per item (executive two-liner and analyst card) and enforced a reflection checklist so outputs consistently included who/what/impact/source. I produced embeddings for semantic search and dedup, and I versioned prompts and model IDs in SSM, logging token usage per call. Every LLM output passed a JSON-Schema validation step; failures, PII findings, or suspicious CVE formats triggered Human Escalation automatically. I also kept a small “golden set” of seeded dupes and fake CVEs to regression-test prompts and thresholds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security and networking&lt;/strong&gt; were explicit. I authenticated users through Cognito user and identity pools and authorized them with group roles (Analyst/Admin) mapped to least-privilege IAM policies. I stored secrets in Secrets Manager (and encrypted everything with KMS) and placed WAF in front of Amplify/API Gateway, with usage plans and rate limits. I used Gateway VPC endpoints for S3 and DynamoDB and added interface endpoints selectively (Bedrock/OpenSearch) where the security benefit outweighed their per-hour cost. I documented a PII policy: I kept raw HTML in a restricted S3 prefix, stored normalized/redacted text separately, applied tight access controls, and retained artifacts only as long as needed.&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%2Fzz72a9fqm0ox0r2aanpb.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%2Fzz72a9fqm0ox0r2aanpb.png" alt="Cognito and Amplify architecture" width="800" height="222"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Observability and operations&lt;/strong&gt; were top priority. I standardized on structured JSON logs with correlation IDs flowing from EventBridge through Step Functions, Lambdas, and agent tool calls, and I enabled X-Ray tracing end-to-end. I tracked SLOs and KPIs—duplicate detection precision, auto-publish precision, p95 latency per stage, and cost per article—and I wired CloudWatch alarms to anomalies and DLQs. I added daily and monthly cost monitors, and I wrote short runbooks for common incidents (OpenSearch throttling, SES sandbox, token bursts).&lt;/p&gt;

&lt;p&gt;I also covered &lt;strong&gt;reliability and data protection&lt;/strong&gt;. I enabled DynamoDB PITR, S3 versioning, and OpenSearch snapshots, and I documented RPO/RTO targets (≤15 minutes for metadata, ≤24 hours for search if I restored from snapshots). In a degraded state, I allowed the system to bypass semantic dedup and fall back to heuristic matching so ingestion never fully stalled.&lt;/p&gt;

&lt;p&gt;On the &lt;strong&gt;product and API&lt;/strong&gt; side, I clarified contracts. I exposed clean endpoints with pagination, filters, and error schemas. For exports, I generated XLSX in a worker pattern that wrote to S3 and returned pre-signed URLs, so large batches didn’t hit Lambda memory/timeouts. In the Amplify app I added a chat UI for natural-language queries with citations, a review queue with decision traces, and threaded commentary. I hardened the NL interface against prompt injection by allow-listing data sources, stripping HTML/JS from prompts, and refusing unsafe actions.&lt;/p&gt;

&lt;p&gt;Finally, I shipped it &lt;strong&gt;as code&lt;/strong&gt;. I organized Terraform into modules with remote state and environment isolation, hashed Lambda artifacts for deterministic deploys, and used canary releases for riskier changes. I tagged everything for cost allocation and preserved a full audit trail—who approved or rejected, which prompt/version ran, and the exact tool call sequence and parameters—for a defined retention window. The result read well in a demo and behaved like production: buffered, idempotent, observable, secure, and ready to toggle between deterministic pipelines and fully agentic orchestration.&lt;/p&gt;

&lt;h1&gt;
  
  
  Use of Strands
&lt;/h1&gt;

&lt;p&gt;I used Strands as the authoring/orchestration layer to define two agents—Ingestor Agent and Analyst Assistant Agent—including their roles, instructions, and the tool bindings to my Lambda “tools” (FeedParser, RelevancyEvaluator, DedupTool, GuardrailTool, StorageTool, HumanEscalation, Notifier, QueryKB, etc.). &lt;/p&gt;

&lt;p&gt;Strands packages those definitions and deploys them to Bedrock AgentCore so they can run at scale with standardized tool I/O, built-in observability, and clean A2A (agent-to-agent) patterns. In short: Strands is where I declare what each agent knows and &lt;strong&gt;which&lt;/strong&gt; tools it can call; AgentCore is where they &lt;strong&gt;run&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I was using feature flags in my deployment to allow easy rollback as well as phased deployment of the features. Here is how my agents worked before and after deployment to Agentcore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Before AgentCore (direct orchestration)&lt;/strong&gt;: Step Functions call my Lambdas directly in a deterministic pipeline (fetch → normalize → evaluate relevance/entities → dedupe → summarize/guardrail → decide publish/review/drop). This let me validate logic, data models, and infra without introducing another moving part. The “agent shim” simply proxied to those Lambdas so the Step Functions contract never changes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;After AgentCore (agentic orchestration)&lt;/strong&gt;: I flipped a flag and Step Functions (or API Gateway for chat) invokes the Strands-defined agents on Bedrock AgentCore. The Ingestor Agent plans and chooses which Lambda tools to call (ReAct + reflection), applies guardrails, clusters duplicates, and returns a triage decision; the Analyst Assistant Agent serves NL queries from Amplify, pulling from DynamoDB/OpenSearch, posting commentary, and even coordinating with the Ingestor via A2A for duplicate context. Functionally it’s the same tools, but now the agent decides when/why to call them.&lt;/p&gt;
&lt;h1&gt;
  
  
  Use of Bedrock
&lt;/h1&gt;

&lt;p&gt;Bedrock underpins every intelligent step:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reasoning + tool use (Agents)&lt;/strong&gt;: Both Strands-defined agents run on Bedrock AgentCore to plan, call tools, and maintain context (ReAct + reflection).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Relevance &amp;amp; entity extraction&lt;/strong&gt;: LLM calls score relevance to your topic taxonomy and extract structured entities (CVEs, actors, malware, vendors, products), emitting JSON with confidence and rationale.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Summarization with reflection&lt;/strong&gt;: The agent (or a summarizer tool) produces an executive 2-liner and an analyst card; a reflection checklist enforces “who/what/impact/source” and validates entity formatting.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Embeddings for semantic dedup/search&lt;/strong&gt;: Bedrock embeddings vectorize normalized content; OpenSearch Serverless k-NN handles near-duplicate detection and semantic retrieval.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Guardrails support&lt;/strong&gt;: While PII and schema checks run in Lambda, the LLM is steered to reduce sensationalism and format errors; suspect outputs route to review.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Conversational NL queries&lt;/strong&gt;: The Analyst Assistant uses Bedrock to interpret questions, translate to DynamoDB/OpenSearch queries, and generate cited answers (and optionally initiate exports).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&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%2Faty01o2pw4uqol2g9fuy.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%2Faty01o2pw4uqol2g9fuy.png" alt="Bedrock infra architecture diagram" width="800" height="396"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Human in the loop
&lt;/h1&gt;

&lt;p&gt;When the Ingestor Agent (or the direct pipeline pre-AgentCore) isn’t fully confident—e.g., borderline relevance, suspected hallucinated CVE, PII detection—it escalates to review. Those items land in the Amplify review queue where an analyst can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;open the decision trace (what tools were called and why),&lt;/li&gt;
&lt;li&gt;approve/reject or edit tags/summaries,&lt;/li&gt;
&lt;li&gt;leave threaded commentary (stored in DynamoDB),&lt;/li&gt;
&lt;li&gt;and provide thumbs up/down feedback that is logged for continuous improvement.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Approved items publish immediately; rejected items are archived with rationale for future training/tuning. The Analyst Assistant Agent also helps humans explore dup clusters, ask trend questions, and post comments via Natural Language.&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%2Fei5vxlbjls608v5s2f81.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%2Fei5vxlbjls608v5s2f81.png" alt="Dashboard" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Challenges and breakthroughs
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bursty feeds &amp;amp; reliability&lt;/strong&gt;: Initial direct triggers caused cascading failures under load. Introducing &lt;strong&gt;SQS between stages&lt;/strong&gt;, DLQs, and &lt;strong&gt;idempotency via URL hash&lt;/strong&gt; stabilized the pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Near-duplicate detection&lt;/strong&gt;: Title/URL heuristics weren’t enough. Pairing &lt;strong&gt;Bedrock embeddings&lt;/strong&gt; with OpenSearch k-NN and clustering solved syndicated/rewritten stories; caching by content hash cut cost/latency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guardrails that matter&lt;/strong&gt;: Early LLM runs occasionally hallucinated CVEs and included stray PII. A &lt;strong&gt;JSON Schema validator&lt;/strong&gt;, &lt;strong&gt;PII filters&lt;/strong&gt;, and a &lt;strong&gt;reflection checklist&lt;/strong&gt; reduced errors and routed edge cases to review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent flipover&lt;/strong&gt;: Moving from Step Functions → Lambdas to AgentCore risked churn. A thin &lt;strong&gt;agent shim&lt;/strong&gt; and a simple &lt;strong&gt;feature flag&lt;/strong&gt; delivered a zero-drama cutover (and instant fallback).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exports at scale&lt;/strong&gt;: XLSX generation hit Lambda limits. Switching to an &lt;strong&gt;async export worker&lt;/strong&gt; that writes to S3 and returns a pre-signed URL made large reports reliable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost visibility&lt;/strong&gt;: Token use and vector storage spiked during spikes. Adding &lt;strong&gt;token budgets&lt;/strong&gt;, embedding caching, and cost per article metrics made FinOps actionable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kiro hooks in practice&lt;/strong&gt;: Instrumenting prompts and tool calls with Kiro hooks gave clean &lt;strong&gt;traceability&lt;/strong&gt; for demos and debugging.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Key learnings
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ship boring first&lt;/strong&gt;: A deterministic pipeline (without agents) is the best baseline for correctness, tests, and rollbacks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agents as an overlay&lt;/strong&gt;: Treat agents as &lt;strong&gt;pluggable orchestrators&lt;/strong&gt; over stable tools; keep I/O contracts tight and versioned.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feature flags are product features&lt;/strong&gt;: enable_agents, enable_opensearch, guardrail levels, and digests let you &lt;strong&gt;canary&lt;/strong&gt; safely and roll back instantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability is a graph problem&lt;/strong&gt;: Backpressure, retries, DLQs, and idempotency must be &lt;strong&gt;end-to-end&lt;/strong&gt;, not per function.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure what you promise&lt;/strong&gt;: SLOs (dup precision, auto-publish precision, p95 latency, cost/article) drive better architectural choices than gut feel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security posture is layered&lt;/strong&gt;: Cognito authZ + least privilege, KMS everywhere, WAF, Secrets Manager rotation, and clear PII retention policies matter in real orgs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search is product, not plumbing&lt;/strong&gt;: Hybrid &lt;strong&gt;BM25 + vectors&lt;/strong&gt;, synonyms, and recency boosts directly impact analyst happiness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small golden datasets pay off&lt;/strong&gt;: A handful of labeled dupes, fake CVEs, and PII cases catch regressions early and keep prompts honest.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Future Plans
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Enrichment &amp;amp; intel quality&lt;/strong&gt;: Integrate KEV/EPSS/NVD lookups, vendor advisories, and STIX/TAXII feeds; auto-normalize vendors/products; add IOC extraction and de-dup across entities, not just articles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evaluation &amp;amp; guardrails maturity&lt;/strong&gt;: Build a small gold dataset (true dupes, fake CVEs, PII cases) and run scheduled evals; add prompt A/B testing, drift detection for embeddings, and policy-as-code for guardrails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agentic depth&lt;/strong&gt;: Introduce planning memory (per-topic context), multi-turn self-verification (“second opinion” model), and a research sub-agent to cross-source claims before auto-publish.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human workflow &amp;amp; governance&lt;/strong&gt;: Add SLAs/priority queues, multi-approver rules for high-impact stories, granular roles/permissions, and full audit export (JSONL) to S3 for compliance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Product UX&lt;/strong&gt;: Faceted search (tags/entities/time/source), cluster views for dup families, inline diff of similar articles, saved queries, and per-team digests; async XLSX/CSV with presets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search relevance&lt;/strong&gt;: OpenSearch synonyms for vendor/product aliases, recency boosting, hybrid BM25+vector reranking, and feedback-driven learning-to-rank.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost &amp;amp; FinOps&lt;/strong&gt;: Track cost per processed article and per published item; autoscale OpenSearch collections; cache embeddings by hash; token budgets per source; nightly right-sizing reports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-tenancy &amp;amp; data boundaries&lt;/strong&gt;: Partition DynamoDB/OpenSearch by tenant (PK prefix), isolate KMS keys, and add per-tenant throttles/quotas for fairness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platform &amp;amp; delivery&lt;/strong&gt;: Canary deploys for Lambdas/agents, blue-green for Step Functions, schema registry + contract tests for tool I/O, and one-click backfill/replay tooling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security posture&lt;/strong&gt;: Secrets Manager rotation, WAF rules for bot mitigation, DLP on raw S3 prefixes with automated quarantine, SBOM/supply-chain scanning, and optional private CA for mTLS between services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integrations&lt;/strong&gt;: Slack/Teams notifications with approve/reject actions, Jira/ServiceNow ticket hooks for critical items, and webhooks for downstream dashboards.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;Sentinel proved that you can take a messy, high-volume RSS firehose and ship a &lt;strong&gt;reliable, secure, and explainable pipeline&lt;/strong&gt; that analysts actually want to use. The key was sequencing: build a &lt;strong&gt;buffered, idempotent&lt;/strong&gt; backbone; define &lt;strong&gt;clear tool contracts&lt;/strong&gt;; then layer on &lt;strong&gt;agentic behavior&lt;/strong&gt; for planning and tool use. With Strands + Bedrock AgentCore, I kept autonomy where it helps (reasoning, tool selection) and guardrails where it counts (schemas, PII checks, human review). From here, the roadmap is about depth, not breadth: richer enrichment (KEV/EPSS/NVD), stronger evaluation loops, hybrid search relevance, and governance (SLAs, multi-approver flows). The system is already production-shaped—now it’s about making it &lt;strong&gt;smarter, cheaper, and harder to break&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>bedrock</category>
      <category>awsstrands</category>
      <category>kiro</category>
    </item>
    <item>
      <title>Keeping your Streamlit app awake using Selenium and Github Actions</title>
      <dc:creator>Benson King'ori</dc:creator>
      <pubDate>Fri, 29 Aug 2025 12:56:15 +0000</pubDate>
      <link>https://dev.to/virgoalpha/keeping-your-streamlit-app-awake-using-selenium-and-github-actions-4ajd</link>
      <guid>https://dev.to/virgoalpha/keeping-your-streamlit-app-awake-using-selenium-and-github-actions-4ajd</guid>
      <description>&lt;h1&gt;
  
  
  TLDR;
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Streamlit apps sleep after a period of inactivity if hosted on Streamlit Community Cloud (free tier)&lt;/li&gt;
&lt;li&gt;To wake your app up, you need to click a button&lt;/li&gt;
&lt;li&gt;We can use Github actions + Selenium to automate this button clicking every couple of hours&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Table of Contents
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;
Introduction
&lt;/li&gt;
&lt;li&gt;
Step 1: Create Repo
&lt;/li&gt;
&lt;li&gt;
Step 2: Create Python Script
&lt;/li&gt;
&lt;li&gt;
Step 3: Create Github Workflow
&lt;/li&gt;
&lt;li&gt;
Step 4: Commit and Push
&lt;/li&gt;
&lt;li&gt;
Step 5: Run the workflow manually
&lt;/li&gt;
&lt;li&gt;
Conclusion
&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;Streamlit apps hosted on the Community Edition (free tier) go to sleep after some period of inactivity. This used to be 24 hours during weekdays and 72 hours on weekends but it had been cut down to 12 hours and that number might even be lower now. Given how we have used Streamlit for project demos and portfolio pages, this is quite a concern. In this article, I explore how to use Github actions to run a script every 4 hours that keep your Streamlit app from going to sleep.&lt;/p&gt;

&lt;h1&gt;
  
  
  Step 1: Create Repo
&lt;/h1&gt;

&lt;p&gt;On your Github account, create a new repo and pull it locally in your desired folder.&lt;/p&gt;

&lt;h1&gt;
  
  
  Step 2: Create Python Script
&lt;/h1&gt;

&lt;p&gt;In your local repository, paste the following code in main.py file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from webdriver_manager.chrome import ChromeDriverManager
from selenium.common.exceptions import TimeoutException
import os

&lt;span class="c"&gt;# Streamlit app URL from environment variable (or default)&lt;/span&gt;
STREAMLIT_URL &lt;span class="o"&gt;=&lt;/span&gt; os.environ.get&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"STREAMLIT_APP_URL"&lt;/span&gt;, &lt;span class="s2"&gt;"https://benson-mugure-portfolio.streamlit.app/"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

def main&lt;span class="o"&gt;()&lt;/span&gt;:
    options &lt;span class="o"&gt;=&lt;/span&gt; Options&lt;span class="o"&gt;()&lt;/span&gt;
    options.add_argument&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'--headless=new'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    options.add_argument&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'--no-sandbox'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    options.add_argument&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'--disable-dev-shm-usage'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    options.add_argument&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'--disable-gpu'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    options.add_argument&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'--window-size=1920,1080'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

    driver &lt;span class="o"&gt;=&lt;/span&gt; webdriver.Chrome&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Service&lt;span class="o"&gt;(&lt;/span&gt;ChromeDriverManager&lt;span class="o"&gt;()&lt;/span&gt;.install&lt;span class="o"&gt;())&lt;/span&gt;, &lt;span class="nv"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;options&lt;span class="o"&gt;)&lt;/span&gt;

    try:
        driver.get&lt;span class="o"&gt;(&lt;/span&gt;STREAMLIT_URL&lt;span class="o"&gt;)&lt;/span&gt;
        print&lt;span class="o"&gt;(&lt;/span&gt;f&lt;span class="s2"&gt;"Opened {STREAMLIT_URL}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

        &lt;span class="nb"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; WebDriverWait&lt;span class="o"&gt;(&lt;/span&gt;driver, 15&lt;span class="o"&gt;)&lt;/span&gt;
        try:
            &lt;span class="c"&gt;# Look for the wake-up button&lt;/span&gt;
            button &lt;span class="o"&gt;=&lt;/span&gt; wait.until&lt;span class="o"&gt;(&lt;/span&gt;
                EC.element_to_be_clickable&lt;span class="o"&gt;((&lt;/span&gt;By.XPATH, &lt;span class="s2"&gt;"//button[contains(text(),'Yes, get this app back up')]"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;)&lt;/span&gt;
            print&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Wake-up button found. Clicking..."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            button.click&lt;span class="o"&gt;()&lt;/span&gt;

            &lt;span class="c"&gt;# After clicking, check if it disappears&lt;/span&gt;
            try:
                wait.until&lt;span class="o"&gt;(&lt;/span&gt;EC.invisibility_of_element_located&lt;span class="o"&gt;((&lt;/span&gt;By.XPATH, &lt;span class="s2"&gt;"//button[contains(text(),'Yes, get this app back up')]"&lt;/span&gt;&lt;span class="o"&gt;)))&lt;/span&gt;
                print&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Button clicked and disappeared ✅ (app should be waking up)"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            except TimeoutException:
                print&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Button was clicked but did NOT disappear ❌ (possible failure)"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;1&lt;span class="o"&gt;)&lt;/span&gt;

        except TimeoutException:
            &lt;span class="c"&gt;# No button at all → app is assumed to be awake&lt;/span&gt;
            print&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"No wake-up button found. Assuming app is already awake ✅"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

    except Exception as e:
        print&lt;span class="o"&gt;(&lt;/span&gt;f&lt;span class="s2"&gt;"Unexpected error: {e}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;1&lt;span class="o"&gt;)&lt;/span&gt;
    finally:
        driver.quit&lt;span class="o"&gt;()&lt;/span&gt;
        print&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Script finished."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;__name__ &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"__main__"&lt;/span&gt;:
    main&lt;span class="o"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the value of the STREAMLIT_URL variable with your own app’s URL as a string.&lt;/p&gt;

&lt;h1&gt;
  
  
  Step 3: Create Github Workflow
&lt;/h1&gt;

&lt;p&gt;Also in your local repository, create the file &lt;code&gt;.github/workflows/wake.yml&lt;/code&gt; and paste the code below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;name: Wake Streamlit App

on:
  schedule:
    - cron: &lt;span class="s2"&gt;"0 */4 * * *"&lt;/span&gt;   &lt;span class="c"&gt;# every 4 hours&lt;/span&gt;
  workflow_dispatch:         &lt;span class="c"&gt;# allow manual trigger&lt;/span&gt;

&lt;span class="nb"&gt;jobs&lt;/span&gt;:
  wake:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repo
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: &lt;span class="s2"&gt;"3.10"&lt;/span&gt;

      - name: Install dependencies
        run: |
          python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--upgrade&lt;/span&gt; pip
          pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

      - name: Run Selenium script
        run: python main.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the base of your repository, add a &lt;code&gt;requirements.txt&lt;/code&gt; file with the following contents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;selenium
webdriver-manager
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Step 4: Commit and Push
&lt;/h1&gt;

&lt;p&gt;Run the following commands to add and commit your changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; “add files”
git push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Needless to say, you can modify the commit message as you wish&lt;/p&gt;

&lt;h1&gt;
  
  
  Step 5: Run the workflow manually
&lt;/h1&gt;

&lt;p&gt;Go to your repository on Github and confirm that the changes have been made, i.e., your files have been pushed. On that repo, click on the Actions tab as can be seen below:&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%2Fegfmxrll6s3kegjcfjyx.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%2Fegfmxrll6s3kegjcfjyx.png" alt="Github Menu" width="800" height="140"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You should be able to see the &lt;code&gt;Wake Streamlit App&lt;/code&gt; workflow on the right, right under All Workflows. Click on the &lt;code&gt;Wake Streamlit App&lt;/code&gt;. On the right, you should see a button that says &lt;code&gt;Run Workflow&lt;/code&gt;. Click it. Another green button with the same label appears. Click it too. You will now see the workflow is in progress. This takes about 2 minutes. After the time has lapsed, check if your Streamlit app is awake. If not, check the logs on Github and debug.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;And there you have it, a simple and free way to keep your Streamlit app awake (hopefully forever). You can find the full code example in this repository &lt;a href="https://github.com/Virgo-Alpha/Coffee" rel="noopener noreferrer"&gt;here&lt;/a&gt;. There is also an option to modify this workflow so that it makes an empty commit to the repository that the Streamlit app is deployed from in that same branch. Perhaps you can explore this option as well if you are feeling adventurous. Empty commits do not necessarily wake your app up but they do reset the clock that is counting down idle time to set your app to sleep. Let me know if you found this article helpful.&lt;/p&gt;

</description>
      <category>selenium</category>
      <category>githubactions</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Mastering Google Apps Script: Free Automation in Google Workspace</title>
      <dc:creator>Benson King'ori</dc:creator>
      <pubDate>Sun, 03 Aug 2025 13:51:59 +0000</pubDate>
      <link>https://dev.to/virgoalpha/mastering-google-apps-script-free-automation-in-google-workspace-3g1e</link>
      <guid>https://dev.to/virgoalpha/mastering-google-apps-script-free-automation-in-google-workspace-3g1e</guid>
      <description>&lt;h1&gt;
  
  
  TLDR;
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Google Apps Script is a serverless service by Google that facilitates automation of workflows in Google suite&lt;/li&gt;
&lt;li&gt;It uses Javascript and the scripts can be run using triggers&lt;/li&gt;
&lt;li&gt;It is limited by various factors such as lack of a package manager and execution time limits&lt;/li&gt;
&lt;li&gt;Apps Script projects can be Bound Scripts, directly linked to a specific Google file, or Standalone Scripts, existing independently in Google Drive, with each type suited for different purposes&lt;/li&gt;
&lt;li&gt;For practical usage without the theory, you can skip right onto the case study.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Table Of Contents
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;li&gt;Sample Use Cases&lt;/li&gt;
&lt;li&gt;Triggers&lt;/li&gt;
&lt;li&gt;Core Services&lt;/li&gt;
&lt;li&gt;Bound vs Stand Alone Scripts&lt;/li&gt;
&lt;li&gt;Handling Environment Variables&lt;/li&gt;
&lt;li&gt;Project Management&lt;/li&gt;
&lt;li&gt;Limitations&lt;/li&gt;
&lt;li&gt;Case Study&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;I was looking for a way to automate some processes in Google Workspace and thought this was a good use case to try out n8n or Zapier. However, I took a step back and wondered if there was a free solution within the Google suite. That’s how I stumbled upon Google Apps Script and decided to explore it. In this article, I go through the main features, limitations and use cases of Google Apps Script then I demonstrate using sample code how one would go about automating a certain monthly financial process.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Google Apps Script?
&lt;/h2&gt;

&lt;p&gt;Google Apps Script is a cloud-based scripting platform based on JavaScript that lets you automate, integrate, and extend Google Workspace applications. It's "serverless," meaning you don't need to worry about hosting or infrastructure. Google handles it all. This makes it incredibly accessible.&lt;/p&gt;

&lt;h1&gt;
  
  
  Sample Use Cases
&lt;/h1&gt;

&lt;p&gt;The big 3 use cases for Google Apps Script are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Automation: The most common use. Automate repetitive tasks like sending templated emails, generating reports in Sheets from data, or organizing files in Drive.&lt;/li&gt;
&lt;li&gt;Integration: Connect different Google services. For example, automatically create a Calendar event from a Google Form submission, or log Gmail attachments into a Google Sheet.&lt;/li&gt;
&lt;li&gt;Customization: Extend the user interface of Google Workspace. You can create custom menus, dialogs, and sidebars in Sheets, Docs, and Forms to build custom workflows for users.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The above offer endless possibilities in both business and personal areas. A few of the ones that I thought of were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automatically saving email attachments to a folder and alerting the user, as well as updating a spreadsheet&lt;/li&gt;
&lt;li&gt;Making daily API calls to a weather site to see if there is a torrential rain warning or cyclone and alerting the user via an email if there is&lt;/li&gt;
&lt;li&gt;Creating custom menu in google sheets where a single click generates a report and sends it to clients in a customized mail merge kind of way&lt;/li&gt;
&lt;li&gt;Google form validation&lt;/li&gt;
&lt;li&gt;Automatically add invites from a certain email address into my calendar&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Triggers
&lt;/h1&gt;

&lt;p&gt;Triggers are what make your scripts run automatically in response to specific events.&lt;br&gt;
Types of triggers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Simple Triggers: Easy-to-use, built-in functions like onOpen() (runs when a document is opened) and onEdit() (runs when a cell is edited).&lt;/li&gt;
&lt;li&gt;Installable Triggers: More powerful and flexible. These can be time-driven (e.g., run a script every morning at 9 AM) or event-driven (e.g., run a script when a Google Form is submitted).
In order to automate your scripts, you will need to add a new trigger from the menu bar found on the left as can be seen below.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once you go to that page, on the bottom right, you will see a button to add a trigger. Clicking that button opens the modal below.&lt;/p&gt;

&lt;p&gt;The trigger can be time driven or calendar driven. The time driven option gives the following categories for timers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Specific date and time&lt;/li&gt;
&lt;li&gt;Minutes timer&lt;/li&gt;
&lt;li&gt;Hours timer&lt;/li&gt;
&lt;li&gt;Day timer&lt;/li&gt;
&lt;li&gt;Week timer&lt;/li&gt;
&lt;li&gt;Month timer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These timers allow you to now select how often the script should run, e.g., every 5 minutes for the minutes timer or Every Monday for the week timer.&lt;/p&gt;
&lt;h2&gt;
  
  
  Pitfalls to Watch Out For
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Time-driven triggers can fail silently if the script takes too long or errors out.&lt;/li&gt;
&lt;li&gt;Installable triggers require authorization—if not granted properly, they won’t run.&lt;/li&gt;
&lt;li&gt;Google may throttle or delay time-based executions under heavy load or policy violations.&lt;/li&gt;
&lt;li&gt;Always monitor the Executions panel for logs and failures.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;
  
  
  Core Services
&lt;/h1&gt;

&lt;p&gt;These are the built-in libraries that allow your script to interact with Google services. You don't need to import anything; they are just available. However, you may need to enable them or add them to your project.&lt;br&gt;
Key services include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SpreadsheetApp: For reading, writing, and formatting data in Google Sheets.&lt;/li&gt;
&lt;li&gt;GmailApp: For reading, searching, and sending emails.&lt;/li&gt;
&lt;li&gt;DocumentApp: For creating and editing Google Docs.&lt;/li&gt;
&lt;li&gt;DriveApp: For managing files and folders in Google Drive.&lt;/li&gt;
&lt;li&gt;UrlFetchApp: For connecting to external, third-party APIs on the internet.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;
  
  
  Bound Vs Stand Alone Scripts
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bound Scripts&lt;/strong&gt;: These are linked directly to a specific Google Sheet, Doc, or Form. They are best for scripts that are only meant to work with that one file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standalone Scripts&lt;/strong&gt;: These exist as their own independent files in Google Drive. They are better for general-purpose scripts or for building web apps and add-ons.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Deployment Considerations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Bound Scripts are easier to deploy for quick file-specific automations.&lt;/li&gt;
&lt;li&gt;Standalone Scripts are necessary for publishing web apps, libraries, or add-ons, and for handling broader integrations across multiple services and files.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;
  
  
  Handling Environment variables
&lt;/h1&gt;

&lt;p&gt;When working with sensitive data such as API keys or tokens, &lt;strong&gt;never hardcode credentials directly into your code&lt;/strong&gt;. Doing so risks exposing them—especially if your script is shared or published as a web app. Instead, use the PropertiesService to securely store and access secrets.&lt;/p&gt;

&lt;p&gt;This approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keeps your credentials separate from your code logic.&lt;/li&gt;
&lt;li&gt;Prevents accidental leaks in version control or shared scripts.&lt;/li&gt;
&lt;li&gt;Makes it easier to manage and rotate secrets without editing source files.
## Step 1: Store the Secret
Create a separate function to set your secret. You only need to run this function once manually from the script editor to save the key.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;function &lt;/span&gt;storeApiKey&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  // Get the script private properties store
  const scriptProperties &lt;span class="o"&gt;=&lt;/span&gt; PropertiesService.getScriptProperties&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  // Set a key-value pair &lt;span class="k"&gt;for &lt;/span&gt;your secret
  scriptProperties.setProperty&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'MY_API_KEY'&lt;/span&gt;, &lt;span class="s1"&gt;'your-secret-api-key-goes-here'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  Logger.log&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"API Key has been stored securely."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Step 2: Retrieve the Secret in Your Code
&lt;/h2&gt;

&lt;p&gt;In your main functions, you can then retrieve the key without ever exposing it in the script itself.&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="k"&gt;function &lt;/span&gt;makeApiCall&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  // Get the script properties store
  const scriptProperties &lt;span class="o"&gt;=&lt;/span&gt; PropertiesService.getScriptProperties&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  // Retrieve the stored secret by its key
  const apiKey &lt;span class="o"&gt;=&lt;/span&gt; scriptProperties.getProperty&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'MY_API_KEY'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  // Now you can use the apiKey variable &lt;span class="k"&gt;in &lt;/span&gt;your API call
  const url &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;https://api.example.com/data?key&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;apiKey&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  const response &lt;span class="o"&gt;=&lt;/span&gt; UrlFetchApp.fetch&lt;span class="o"&gt;(&lt;/span&gt;url&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  Logger.log&lt;span class="o"&gt;(&lt;/span&gt;response.getContentText&lt;span class="o"&gt;())&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This method ensures your sensitive information is kept separate from your code, which is essential for security.&lt;/p&gt;

&lt;h1&gt;
  
  
  Project management
&lt;/h1&gt;

&lt;p&gt;One Apps script project can have multiple files which can be triggered separately but cannot have different declarations of variable names.&lt;br&gt;
All script files (.gs) within a single Apps Script project are executed in one shared global scope. Think of it as Google taking all your separate files, concatenating them into one large file behind the scenes, and then running it.&lt;br&gt;
This is why you can't redeclare a variable with const or let in another file—from the engine's perspective, you're trying to declare the same variable twice in the same script. This global nature is also what makes calling functions between files so seamless.&lt;/p&gt;
&lt;h2&gt;
  
  
  Considerations for Splitting a Project into Multiple Files
&lt;/h2&gt;

&lt;p&gt;Splitting your code is purely for organization and readability. It has no effect on how the code runs. Here are a few things to consider before you do it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Logical Separation: Group related functions into the same file. For example, have one file for all functions that interact with Google Sheets (sheets.gs), another for Gmail logic (gmail.gs), and a main file for the primary workflow (main.gs).&lt;/li&gt;
&lt;li&gt;Configuration: Keep global constants and configuration settings (like spreadsheet IDs, email addresses, or API keys stored in Properties Service) in a dedicated file (e.g., config.gs). This makes them easy to find and update.&lt;/li&gt;
&lt;li&gt;Maintainability: For large projects, splitting the code makes it much easier to navigate, debug, and for other people to understand. A single 1,000-line file is much harder to work with than five 200-line files with clear purposes.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  One Project vs. Multiple Projects
&lt;/h2&gt;

&lt;p&gt;The decision to keep code in one project or split it into different projects depends on the tasks you are automating.&lt;br&gt;
Keep it in one project if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The scripts are part of a single, cohesive workflow (e.g., reading from a Sheet, processing the data, and sending an email).&lt;/li&gt;
&lt;li&gt;The functions in different files need to call each other or share global variables.&lt;/li&gt;
&lt;li&gt;The entire workflow can operate under a single set of permissions (e.g., the whole project needs access to both Sheets and Gmail).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Split it into multiple projects if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The automations are completely unrelated (e.g., one script organizes your Drive, and another sends you a daily weather report).&lt;/li&gt;
&lt;li&gt;The automations require different security permissions. Separating them ensures one script doesn't have access to services it doesn't need (e.g., one script only needs access to a specific Sheet, while another needs access to your entire Calendar).&lt;/li&gt;
&lt;li&gt;They run on completely independent triggers and have no logical connection.
## How Different Files Interact
Because all files share the same global environment, calling a function from another file is effortless. You just call it directly by name as if it were in the same file.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;
  
  
  Limitations
&lt;/h1&gt;
&lt;h2&gt;
  
  
  1. Cannot Decrypt Password-Protected Files
&lt;/h2&gt;

&lt;p&gt;A script can see a password-protected file in Google Drive, but it cannot open or read its contents. The Apps Script environment has no built-in mechanism to supply a password to decrypt a file, which is why a more capable environment like a Python script or Google Cloud Function is required for this task.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Limited Native File Processing
&lt;/h2&gt;

&lt;p&gt;Apps Script cannot natively parse complex file formats like PDFs or Excel spreadsheets to extract data directly. While it can convert some files to Google Workspace formats (e.g., PDF to Google Doc), it doesn't offer granular control to read the raw data or structure from the original file itself.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Execution Time Limits
&lt;/h2&gt;

&lt;p&gt;Scripts have a maximum execution time. For most standard Google accounts (including Gmail and Google Workspace), this limit is 6 minutes per run. For long-running tasks like processing hundreds of files or spreadsheet rows, your script may time out before it can finish.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Service Quotas and Rate Limits
&lt;/h2&gt;

&lt;p&gt;To prevent abuse, Google imposes daily quotas and rate limits on the use of its services. For example, there are limits on how many emails GmailApp can send per day, how many API calls you can make, or how many triggers can run. For large-scale automations, you can hit these limits.&lt;/p&gt;
&lt;h2&gt;
  
  
  5. Sandboxed Environment and No Package Manager
&lt;/h2&gt;

&lt;p&gt;Apps Script runs in a secure, sandboxed environment, which means:&lt;br&gt;
You cannot use standard package managers like npm to import external JavaScript libraries.&lt;br&gt;
You have no direct access to the server's file system or the ability to make arbitrary network connections.&lt;/p&gt;
&lt;h2&gt;
  
  
  6. Simple Trigger Restrictions
&lt;/h2&gt;

&lt;p&gt;Simple triggers like onOpen(e) and onEdit(e) run in a restricted mode. They cannot access any service that requires user authorization. For example, an onEdit trigger cannot send an email or create a calendar event, which is a common source of confusion for new developers.&lt;/p&gt;
&lt;h1&gt;
  
  
  Case Study
&lt;/h1&gt;

&lt;p&gt;The original idea I wanted to automate was that of investing. Every time I get my payslip, I usually save it to a certain folder and then calculate how much of a particular stock I can buy for that month then I go ahead and place the order via email. The below step-by-step guide will show you how to automate this entire workflow. Now, let’s get coding.&lt;/p&gt;

&lt;p&gt;If you want to just straight into the code, find the repository &lt;a href="https://github.com/Virgo-Alpha/Financial_Automation" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1: Initial Setup (Do this first!)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create a New Google Sheet. Name it "My Stock Portfolio".&lt;/li&gt;
&lt;li&gt;Inside the sheet, create two tabs: Trading and Transactions.&lt;/li&gt;
&lt;li&gt;Go to Extensions &amp;gt; Apps Script to open the script editor. This will create a new Apps Script project that is bound to your spreadsheet.&lt;/li&gt;
&lt;li&gt;Get a Free API Key from &lt;a href="https://site.financialmodelingprep.com/developer" rel="noopener noreferrer"&gt;Financial Modeling Prep&lt;/a&gt;. You'll need this for the stock price data.&lt;/li&gt;
&lt;li&gt;Create a Google Drive Folder where your payslips and contract notes will be saved. Right-click the folder and get its ID from the URL.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Step 2: The Code (Create these files in your Apps Script project)
&lt;/h2&gt;

&lt;p&gt;In the Apps Script editor, create the following files by clicking the + icon next to "Files". Copy and paste the code for each one.&lt;br&gt;
Config.gs (Configuration)&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="nt"&gt;---&lt;/span&gt; CONFIGURATION FILE &lt;span class="nt"&gt;---&lt;/span&gt;
// Store all your personal settings here.


const CONFIG &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 // &lt;span class="nt"&gt;---&lt;/span&gt; Email Settings &lt;span class="nt"&gt;---&lt;/span&gt;
 MY_EMAIL: &lt;span class="s2"&gt;"your_email@example.com"&lt;/span&gt;, // Your primary email address
 BROKER_EMAIL: &lt;span class="s2"&gt;"broker@example.com"&lt;/span&gt;,   // Your stockbrokers email address
  // &lt;span class="nt"&gt;---&lt;/span&gt; Payslip Email Settings &lt;span class="nt"&gt;---&lt;/span&gt;
 PAYSLIP_SENDER: &lt;span class="s2"&gt;"payslips@company.com"&lt;/span&gt;,
 PAYSLIP_SUBJECT_CONTAINS: &lt;span class="s2"&gt;"Your Monthly Payslip"&lt;/span&gt;,
  // &lt;span class="nt"&gt;---&lt;/span&gt; Contract Note Email Settings &lt;span class="nt"&gt;---&lt;/span&gt;
 CONTRACT_NOTE_SENDER: &lt;span class="s2"&gt;"contracts@broker.com"&lt;/span&gt;,
 CONTRACT_NOTE_SUBJECT_CONTAINS: &lt;span class="s2"&gt;"Contract Note"&lt;/span&gt;,


 // &lt;span class="nt"&gt;---&lt;/span&gt; Drive Folder &lt;span class="nt"&gt;---&lt;/span&gt;
 FINANCE_FOLDER_ID: &lt;span class="s2"&gt;"YOUR_GOOGLE_DRIVE_FOLDER_ID"&lt;/span&gt;,


 // &lt;span class="nt"&gt;---&lt;/span&gt; Financial Settings &lt;span class="nt"&gt;---&lt;/span&gt;
 MONTHLY_SALARY: 10000,
 INVESTMENT_PERCENTAGE: 0.20, // 20%
 STOCK_TICKER: &lt;span class="s2"&gt;"AAPL"&lt;/span&gt;,
 CDS_ACCOUNT: &lt;span class="s2"&gt;"CDS123456FI00"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Secrets.gs (API Key Management)&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="nt"&gt;---&lt;/span&gt; API KEY MANAGEMENT &lt;span class="nt"&gt;---&lt;/span&gt;
// Use this file to securely store and retrieve your API key.


/&lt;span class="k"&gt;**&lt;/span&gt;
&lt;span class="k"&gt;*&lt;/span&gt; Stores the API key &lt;span class="k"&gt;in &lt;/span&gt;PropertiesService.
&lt;span class="k"&gt;*&lt;/span&gt; Run this &lt;span class="k"&gt;function &lt;/span&gt;ONCE MANUALLY from the editor after pasting your key.
&lt;span class="k"&gt;*&lt;/span&gt;/
&lt;span class="k"&gt;function &lt;/span&gt;storeApiKey&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 const scriptProperties &lt;span class="o"&gt;=&lt;/span&gt; PropertiesService.getScriptProperties&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 // &lt;span class="o"&gt;!!!&lt;/span&gt; PASTE YOUR API KEY HERE &lt;span class="o"&gt;!!!&lt;/span&gt;
 scriptProperties.setProperty&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'FINANCE_API_KEY'&lt;/span&gt;, &lt;span class="s1"&gt;'YOUR_FINANCIAL_MODELING_PREP_API_KEY'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 Logger.log&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"API Key has been stored securely."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;


/&lt;span class="k"&gt;**&lt;/span&gt;
&lt;span class="k"&gt;*&lt;/span&gt; Retrieves the stored API key.
&lt;span class="k"&gt;*&lt;/span&gt; @returns &lt;span class="o"&gt;{&lt;/span&gt;string&lt;span class="o"&gt;}&lt;/span&gt; The API key.
&lt;span class="k"&gt;*&lt;/span&gt;/
&lt;span class="k"&gt;function &lt;/span&gt;getApiKey&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 const scriptProperties &lt;span class="o"&gt;=&lt;/span&gt; PropertiesService.getScriptProperties&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="k"&gt;return &lt;/span&gt;scriptProperties.getProperty&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'FINANCE_API_KEY'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Main.gs (Triggers &amp;amp; Menus)&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="nt"&gt;---&lt;/span&gt; MAIN SCRIPT FILE &lt;span class="nt"&gt;---&lt;/span&gt;
// Contains the main triggers and UI functions.


/&lt;span class="k"&gt;**&lt;/span&gt;
&lt;span class="k"&gt;*&lt;/span&gt; Creates a custom menu &lt;span class="k"&gt;in &lt;/span&gt;the spreadsheet when its opened.
&lt;span class="k"&gt;*&lt;/span&gt;/
&lt;span class="k"&gt;function &lt;/span&gt;onOpen&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 SpreadsheetApp.getUi&lt;span class="o"&gt;()&lt;/span&gt;
   .createMenu&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Stock Trading'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
   .addItem&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'📈 Place New Trade Order'&lt;/span&gt;, &lt;span class="s1"&gt;'showTradeDialog'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
   .addToUi&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;


/&lt;span class="k"&gt;**&lt;/span&gt;
&lt;span class="k"&gt;*&lt;/span&gt; Main &lt;span class="k"&gt;function &lt;/span&gt;to process incoming emails.
&lt;span class="k"&gt;*&lt;/span&gt; Set up a time-driven trigger to run this every 5-10 minutes.
&lt;span class="k"&gt;*&lt;/span&gt;/
&lt;span class="k"&gt;function &lt;/span&gt;processAllEmails&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 Logger.log&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"--- Starting email processing cycle ---"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 processPayslipEmails&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 processContractNoteEmails&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 Logger.log&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"--- Finished email processing cycle ---"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GmailProcessing.gs (Email Handling)&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="nt"&gt;---&lt;/span&gt; EMAIL PROCESSING LOGIC &lt;span class="nt"&gt;---&lt;/span&gt;


/&lt;span class="k"&gt;**&lt;/span&gt;
&lt;span class="k"&gt;*&lt;/span&gt; Processes payslip emails, saves the attachment, and sends a notification.
&lt;span class="k"&gt;*&lt;/span&gt;/
&lt;span class="k"&gt;function &lt;/span&gt;processPayslipEmails&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 const query &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;from:&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CONFIG&lt;/span&gt;&lt;span class="p"&gt;.PAYSLIP_SENDER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; subject:&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CONFIG&lt;/span&gt;&lt;span class="p"&gt;.PAYSLIP_SUBJECT_CONTAINS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; is:unread&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 Logger.log&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;Searching &lt;span class="k"&gt;for &lt;/span&gt;payslips with query: &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;query&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;


 const threads &lt;span class="o"&gt;=&lt;/span&gt; GmailApp.search&lt;span class="o"&gt;(&lt;/span&gt;query&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;threads.length &lt;span class="o"&gt;===&lt;/span&gt; 0&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;


 &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;const thread of threads&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
   const message &lt;span class="o"&gt;=&lt;/span&gt; thread.getMessages&lt;span class="o"&gt;()[&lt;/span&gt;0]&lt;span class="p"&gt;;&lt;/span&gt; // Process first message
   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;message.isUnread&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
     // 1. Save attachment
     const attachment &lt;span class="o"&gt;=&lt;/span&gt; message.getAttachments&lt;span class="o"&gt;()[&lt;/span&gt;0]&lt;span class="p"&gt;;&lt;/span&gt;
     &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;attachment &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; attachment.getContentType&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'application/pdf'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
       const folder &lt;span class="o"&gt;=&lt;/span&gt; DriveApp.getFolderById&lt;span class="o"&gt;(&lt;/span&gt;CONFIG.FINANCE_FOLDER_ID&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
       folder.createFile&lt;span class="o"&gt;(&lt;/span&gt;attachment.copyBlob&lt;span class="o"&gt;())&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
       Logger.log&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;Saved payslip: &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;attachment&lt;/span&gt;&lt;span class="p"&gt;.getName()&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
     &lt;span class="o"&gt;}&lt;/span&gt;

     // 2. Get stock price and calculate investment
     const stockData &lt;span class="o"&gt;=&lt;/span&gt; getStockPrice&lt;span class="o"&gt;(&lt;/span&gt;CONFIG.STOCK_TICKER&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
     &lt;span class="nb"&gt;let &lt;/span&gt;investmentInfo &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Could not retrieve stock price at this time."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
     &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;stockData&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
       const investmentAmount &lt;span class="o"&gt;=&lt;/span&gt; CONFIG.MONTHLY_SALARY &lt;span class="k"&gt;*&lt;/span&gt; CONFIG.INVESTMENT_PERCENTAGE&lt;span class="p"&gt;;&lt;/span&gt;
       const sharesToBuy &lt;span class="o"&gt;=&lt;/span&gt; Math.floor&lt;span class="o"&gt;(&lt;/span&gt;investmentAmount / stockData.price&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
       investmentInfo &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;The current price of &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CONFIG&lt;/span&gt;&lt;span class="p"&gt;.STOCK_TICKER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; is &lt;span class="nv"&gt;$$&lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;stockData.price.toFixed&lt;span class="o"&gt;(&lt;/span&gt;2&lt;span class="o"&gt;)}&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
With 20% of your salary &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$$&lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;investmentAmount.toFixed&lt;span class="o"&gt;(&lt;/span&gt;2&lt;span class="o"&gt;)})&lt;/span&gt;, you could buy approximately &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;sharesToBuy&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; shares.&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
     &lt;span class="o"&gt;}&lt;/span&gt;


     // 3. Send notification email
     const subject &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"✅ Your Payslip Has Been Processed"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
     const body &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;Hello,&lt;span class="se"&gt;\n\n&lt;/span&gt;Your payslip has been saved to Google Drive.&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;investmentInfo&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;Thank you.&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
     GmailApp.sendEmail&lt;span class="o"&gt;(&lt;/span&gt;CONFIG.MY_EMAIL, subject, body&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

     // 4. Mark as &lt;span class="nb"&gt;read &lt;/span&gt;and check the box &lt;span class="k"&gt;in &lt;/span&gt;the sheet
     thread.markRead&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
     updateMonthlyChecklist&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="o"&gt;}&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;


/&lt;span class="k"&gt;**&lt;/span&gt;
&lt;span class="k"&gt;*&lt;/span&gt; Processes contract note emails.
&lt;span class="k"&gt;*&lt;/span&gt;/
&lt;span class="k"&gt;function &lt;/span&gt;processContractNoteEmails&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 const query &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;from:&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CONFIG&lt;/span&gt;&lt;span class="p"&gt;.CONTRACT_NOTE_SENDER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; subject:&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CONFIG&lt;/span&gt;&lt;span class="p"&gt;.CONTRACT_NOTE_SUBJECT_CONTAINS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; is:unread&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 Logger.log&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;Searching &lt;span class="k"&gt;for &lt;/span&gt;contract notes with query: &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;query&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  const threads &lt;span class="o"&gt;=&lt;/span&gt; GmailApp.search&lt;span class="o"&gt;(&lt;/span&gt;query&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;threads.length &lt;span class="o"&gt;===&lt;/span&gt; 0&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;


 &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;const thread of threads&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    const message &lt;span class="o"&gt;=&lt;/span&gt; thread.getMessages&lt;span class="o"&gt;()[&lt;/span&gt;0]&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;message.isUnread&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
       const attachment &lt;span class="o"&gt;=&lt;/span&gt; message.getAttachments&lt;span class="o"&gt;()[&lt;/span&gt;0]&lt;span class="p"&gt;;&lt;/span&gt;
       &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;attachment&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
          const folder &lt;span class="o"&gt;=&lt;/span&gt; DriveApp.getFolderById&lt;span class="o"&gt;(&lt;/span&gt;CONFIG.FINANCE_FOLDER_ID&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          folder.createFile&lt;span class="o"&gt;(&lt;/span&gt;attachment.copyBlob&lt;span class="o"&gt;())&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          Logger.log&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;Saved contract note: &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;attachment&lt;/span&gt;&lt;span class="p"&gt;.getName()&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
       &lt;span class="o"&gt;}&lt;/span&gt;

       // As we cannot parse the PDF, we notify the user to update the sheet.
       const subject &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"📝 Action Required: Log Your Recent Trade"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
       const body &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;Hello,&lt;span class="se"&gt;\n\n&lt;/span&gt;A new contract note has been saved to your Drive.&lt;span class="se"&gt;\n\n&lt;/span&gt;Please open your &lt;span class="s1"&gt;'My Stock Portfolio'&lt;/span&gt; spreadsheet and log the details of this transaction &lt;span class="k"&gt;in &lt;/span&gt;the &lt;span class="s1"&gt;'Transactions'&lt;/span&gt; tab.&lt;span class="se"&gt;\n\n&lt;/span&gt;Thank you.&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
       GmailApp.sendEmail&lt;span class="o"&gt;(&lt;/span&gt;CONFIG.MY_EMAIL, subject, body&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

       thread.markRead&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;


/&lt;span class="k"&gt;**&lt;/span&gt;
&lt;span class="k"&gt;*&lt;/span&gt; Finds the current month/year row &lt;span class="k"&gt;in &lt;/span&gt;the &lt;span class="s1"&gt;'Trading'&lt;/span&gt; sheet and checks the box.
&lt;span class="k"&gt;*&lt;/span&gt;/
&lt;span class="k"&gt;function &lt;/span&gt;updateMonthlyChecklist&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 const sheet &lt;span class="o"&gt;=&lt;/span&gt; SpreadsheetApp.getActiveSpreadsheet&lt;span class="o"&gt;()&lt;/span&gt;.getSheetByName&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Trading"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 const data &lt;span class="o"&gt;=&lt;/span&gt; sheet.getDataRange&lt;span class="o"&gt;()&lt;/span&gt;.getValues&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 const now &lt;span class="o"&gt;=&lt;/span&gt; new Date&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 const monthYear &lt;span class="o"&gt;=&lt;/span&gt; Utilities.formatDate&lt;span class="o"&gt;(&lt;/span&gt;now, Session.getScriptTimeZone&lt;span class="o"&gt;()&lt;/span&gt;, &lt;span class="s2"&gt;"MMMM yyyy"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;


 &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;let &lt;/span&gt;i &lt;span class="o"&gt;=&lt;/span&gt; 1&lt;span class="p"&gt;;&lt;/span&gt; i &amp;lt; data.length&lt;span class="p"&gt;;&lt;/span&gt; i++&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;data[i][0] &lt;span class="o"&gt;===&lt;/span&gt; monthYear&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
     sheet.getRange&lt;span class="o"&gt;(&lt;/span&gt;i + 1, 2&lt;span class="o"&gt;)&lt;/span&gt;.check&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; // Check the box &lt;span class="k"&gt;in &lt;/span&gt;column B
     &lt;span class="nb"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="o"&gt;}&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;StockTrading.gs (UI &amp;amp; Trading Logic)&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="nt"&gt;---&lt;/span&gt; STOCK TRADING UI AND LOGIC &lt;span class="nt"&gt;---&lt;/span&gt;


/&lt;span class="k"&gt;**&lt;/span&gt;
&lt;span class="k"&gt;*&lt;/span&gt; Shows the custom HTML dialog &lt;span class="k"&gt;for &lt;/span&gt;placing a trade.
&lt;span class="k"&gt;*&lt;/span&gt;/
&lt;span class="k"&gt;function &lt;/span&gt;showTradeDialog&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 const html &lt;span class="o"&gt;=&lt;/span&gt; HtmlService.createHtmlOutputFromFile&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Dialog.html'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
     .setWidth&lt;span class="o"&gt;(&lt;/span&gt;400&lt;span class="o"&gt;)&lt;/span&gt;
     .setHeight&lt;span class="o"&gt;(&lt;/span&gt;450&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 SpreadsheetApp.getUi&lt;span class="o"&gt;()&lt;/span&gt;.showModalDialog&lt;span class="o"&gt;(&lt;/span&gt;html, &lt;span class="s1"&gt;'Place a Stock Trade Order'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;


/&lt;span class="k"&gt;**&lt;/span&gt;
&lt;span class="k"&gt;*&lt;/span&gt; Fetches the current stock price to populate the dialog.
&lt;span class="k"&gt;*&lt;/span&gt; This &lt;span class="k"&gt;function &lt;/span&gt;is called from the client-side HTML.
&lt;span class="k"&gt;*&lt;/span&gt;/
&lt;span class="k"&gt;function &lt;/span&gt;getLiveStockPrice&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="k"&gt;return &lt;/span&gt;getStockPrice&lt;span class="o"&gt;(&lt;/span&gt;CONFIG.STOCK_TICKER&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;


/&lt;span class="k"&gt;**&lt;/span&gt;
&lt;span class="k"&gt;*&lt;/span&gt; Processes the trade order submitted from the dialog.
&lt;span class="k"&gt;*&lt;/span&gt; @param &lt;span class="o"&gt;{&lt;/span&gt;object&lt;span class="o"&gt;}&lt;/span&gt; orderDetails An object from the dialog form.
&lt;span class="k"&gt;*&lt;/span&gt;/
&lt;span class="k"&gt;function &lt;/span&gt;placeTradeOrder&lt;span class="o"&gt;(&lt;/span&gt;orderDetails&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 const &lt;span class="o"&gt;{&lt;/span&gt; tradeDirection, quantity, price &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; orderDetails&lt;span class="p"&gt;;&lt;/span&gt;
  const subject &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;Trade Order: &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;tradeDirection&lt;/span&gt;&lt;span class="p"&gt;.toUpperCase()&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;quantity&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CONFIG&lt;/span&gt;&lt;span class="p"&gt;.STOCK_TICKER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; @ &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;price&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 const body &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;
   Hello,


   Please execute the following trade order &lt;span class="k"&gt;for &lt;/span&gt;my account &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CONFIG&lt;/span&gt;&lt;span class="p"&gt;.CDS_ACCOUNT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;:


   &lt;span class="nt"&gt;----------------------------------&lt;/span&gt;
   Security Name:    &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CONFIG&lt;/span&gt;&lt;span class="p"&gt;.STOCK_TICKER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
   Trade Direction:  &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;tradeDirection&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
   Number of Shares: &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;quantity&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; Shares
   Price:            Market or MUR &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;price&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
   Validity:         Maximum 30 days
   &lt;span class="nt"&gt;----------------------------------&lt;/span&gt;


   Thank you.
 &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;


 try &lt;span class="o"&gt;{&lt;/span&gt;
   // Send email to the broker and BCC self.
   GmailApp.sendEmail&lt;span class="o"&gt;(&lt;/span&gt;CONFIG.BROKER_EMAIL, subject, body, &lt;span class="o"&gt;{&lt;/span&gt;
     bcc: CONFIG.MY_EMAIL
   &lt;span class="o"&gt;})&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

   // Log the transaction to the &lt;span class="s1"&gt;'Transactions'&lt;/span&gt; sheet
   const transactionsSheet &lt;span class="o"&gt;=&lt;/span&gt; SpreadsheetApp.getActiveSpreadsheet&lt;span class="o"&gt;()&lt;/span&gt;.getSheetByName&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Transactions"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   transactionsSheet.appendRow&lt;span class="o"&gt;([&lt;/span&gt;new Date&lt;span class="o"&gt;()&lt;/span&gt;, CONFIG.STOCK_TICKER, tradeDirection.toUpperCase&lt;span class="o"&gt;()&lt;/span&gt;, quantity, price, &lt;span class="s2"&gt;"PLACED"&lt;/span&gt;&lt;span class="o"&gt;])&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"✅ Success! Your trade order has been emailed to the broker and logged."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt; catch &lt;span class="o"&gt;(&lt;/span&gt;e&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
   Logger.log&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;Failed to send trade email: &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;e&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;❌ Error: Could not send the trade order. Please check the logs.&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;APIs.gs (External API Calls)&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="nt"&gt;---&lt;/span&gt; EXTERNAL API CALLS &lt;span class="nt"&gt;---&lt;/span&gt;


/&lt;span class="k"&gt;**&lt;/span&gt;
&lt;span class="k"&gt;*&lt;/span&gt; Fetches the latest stock price from Financial Modeling Prep.
&lt;span class="k"&gt;*&lt;/span&gt; @param &lt;span class="o"&gt;{&lt;/span&gt;string&lt;span class="o"&gt;}&lt;/span&gt; ticker The stock symbol &lt;span class="o"&gt;(&lt;/span&gt;e.g., &lt;span class="s2"&gt;"AAPL"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="k"&gt;*&lt;/span&gt; @returns &lt;span class="o"&gt;{&lt;/span&gt;object|null&lt;span class="o"&gt;}&lt;/span&gt; An object with price and volume, or null on error.
&lt;span class="k"&gt;*&lt;/span&gt;/
&lt;span class="k"&gt;function &lt;/span&gt;getStockPrice&lt;span class="o"&gt;(&lt;/span&gt;ticker&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 const apiKey &lt;span class="o"&gt;=&lt;/span&gt; getApiKey&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;apiKey&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
   Logger.log&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"API Key not found. Please run storeApiKey() first."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="k"&gt;return &lt;/span&gt;null&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;
  const url &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;https://financialmodelingprep.com/api/v3/quote-short/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ticker&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;?apikey&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;apiKey&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  try &lt;span class="o"&gt;{&lt;/span&gt;
   const response &lt;span class="o"&gt;=&lt;/span&gt; UrlFetchApp.fetch&lt;span class="o"&gt;(&lt;/span&gt;url, &lt;span class="o"&gt;{&lt;/span&gt; muteHttpExceptions: &lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="o"&gt;})&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   const responseCode &lt;span class="o"&gt;=&lt;/span&gt; response.getResponseCode&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   const content &lt;span class="o"&gt;=&lt;/span&gt; response.getContentText&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;


   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;responseCode &lt;span class="o"&gt;===&lt;/span&gt; 200&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
     const data &lt;span class="o"&gt;=&lt;/span&gt; JSON.parse&lt;span class="o"&gt;(&lt;/span&gt;content&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
     &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;data &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; data.length &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 0&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
       &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; price: data[0].price, volume: data[0].volume &lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
     &lt;span class="o"&gt;}&lt;/span&gt;
   &lt;span class="o"&gt;}&lt;/span&gt;
   Logger.log&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;API Error: Response code &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;responseCode&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; Content: &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="k"&gt;return &lt;/span&gt;null&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt; catch &lt;span class="o"&gt;(&lt;/span&gt;e&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
   Logger.log&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;Failed to fetch stock price: &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;e&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="k"&gt;return &lt;/span&gt;null&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dialog.html (Custom UI)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&amp;lt;&lt;span class="o"&gt;!&lt;/span&gt;DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
 &amp;lt;&lt;span class="nb"&gt;head&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
   &amp;lt;base &lt;span class="nv"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"_top"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
   &amp;lt;&lt;span class="nb"&gt;link &lt;/span&gt;&lt;span class="nv"&gt;rel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"stylesheet"&lt;/span&gt; &lt;span class="nv"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
   &amp;lt;style&amp;gt;
     body &lt;span class="o"&gt;{&lt;/span&gt; padding: 20px&lt;span class="p"&gt;;&lt;/span&gt; font-family: sans-serif&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
     .loader &lt;span class="o"&gt;{&lt;/span&gt;
       border: 4px solid &lt;span class="c"&gt;#f3f3f3;&lt;/span&gt;
       border-radius: 50%&lt;span class="p"&gt;;&lt;/span&gt;
       border-top: 4px solid &lt;span class="c"&gt;#3498db;&lt;/span&gt;
       width: 20px&lt;span class="p"&gt;;&lt;/span&gt;
       height: 20px&lt;span class="p"&gt;;&lt;/span&gt;
       animation: spin 2s linear infinite&lt;span class="p"&gt;;&lt;/span&gt;
       display: inline-block&lt;span class="p"&gt;;&lt;/span&gt;
     &lt;span class="o"&gt;}&lt;/span&gt;
     @keyframes spin &lt;span class="o"&gt;{&lt;/span&gt;
       0% &lt;span class="o"&gt;{&lt;/span&gt; transform: rotate&lt;span class="o"&gt;(&lt;/span&gt;0deg&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
       100% &lt;span class="o"&gt;{&lt;/span&gt; transform: rotate&lt;span class="o"&gt;(&lt;/span&gt;360deg&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
     &lt;span class="o"&gt;}&lt;/span&gt;
     &lt;span class="c"&gt;#status { margin-top: 15px; font-weight: bold; }&lt;/span&gt;
   &amp;lt;/style&amp;gt;
 &amp;lt;/head&amp;gt;
 &amp;lt;body&amp;gt;
   &amp;lt;h4&amp;gt;Place Trade Order&amp;lt;/h4&amp;gt;
   &amp;lt;p&amp;gt;Place a buy or sell order &lt;span class="k"&gt;for&lt;/span&gt; &amp;lt;strong&amp;gt;AAPL&amp;lt;/strong&amp;gt;.&amp;lt;/p&amp;gt;

   &amp;lt;form &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"tradeForm"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
     &amp;lt;div &lt;span class="nv"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"form-group"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
       &amp;lt;label &lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"tradeDirection"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;Action&amp;lt;/label&amp;gt;
       &amp;lt;&lt;span class="k"&gt;select &lt;/span&gt;&lt;span class="nv"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"form-control"&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"tradeDirection"&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"tradeDirection"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
         &amp;lt;option &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Buy"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;Buy&amp;lt;/option&amp;gt;
         &amp;lt;option &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Sell"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;Sell&amp;lt;/option&amp;gt;
       &amp;lt;/select&amp;gt;
     &amp;lt;/div&amp;gt;


     &amp;lt;div &lt;span class="nv"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"form-group"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
       &amp;lt;label &lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"quantity"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;Quantity &lt;span class="o"&gt;(&lt;/span&gt;Number of Shares&lt;span class="o"&gt;)&lt;/span&gt;&amp;lt;/label&amp;gt;
       &amp;lt;input &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"number"&lt;/span&gt; &lt;span class="nv"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"form-control"&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"quantity"&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"quantity"&lt;/span&gt; required&amp;gt;
     &amp;lt;/div&amp;gt;


     &amp;lt;div &lt;span class="nv"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"form-group"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
       &amp;lt;label &lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"price"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;Price &lt;span class="o"&gt;(&lt;/span&gt;USD&lt;span class="o"&gt;)&lt;/span&gt;&amp;lt;/label&amp;gt;
       &amp;lt;div &lt;span class="nv"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"input-group"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
         &amp;lt;input &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"number"&lt;/span&gt; &lt;span class="nv"&gt;step&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"0.01"&lt;/span&gt; &lt;span class="nv"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"form-control"&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"price"&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"price"&lt;/span&gt; required&amp;gt;
         &amp;lt;div &lt;span class="nv"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"input-group-append"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
           &amp;lt;button &lt;span class="nv"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"btn btn-outline-secondary"&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"button"&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"fetchPriceBtn"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;Get Live Price&amp;lt;/button&amp;gt;
         &amp;lt;/div&amp;gt;
       &amp;lt;/div&amp;gt;
       &amp;lt;small &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"priceLoader"&lt;/span&gt; &lt;span class="nv"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"display:none;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;Fetching... &amp;lt;div &lt;span class="nv"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"loader"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;lt;/div&amp;gt;&amp;lt;/small&amp;gt;
     &amp;lt;/div&amp;gt;


     &amp;lt;button &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"submit"&lt;/span&gt; &lt;span class="nv"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"btn btn-primary btn-block"&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"submitBtn"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;Place Order&amp;lt;/button&amp;gt;
   &amp;lt;/form&amp;gt;


   &amp;lt;div &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt; &lt;span class="nv"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"alert"&lt;/span&gt; &lt;span class="nv"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"alert"&lt;/span&gt; &lt;span class="nv"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"display:none;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;lt;/div&amp;gt;


   &amp;lt;script&amp;gt;
     // Fetch live price when button is clicked
     document.getElementById&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"fetchPriceBtn"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;.addEventListener&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"click"&lt;/span&gt;, &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
       document.getElementById&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"priceLoader"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;.style.display &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"block"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
       google.script.run
         .withSuccessHandler&lt;span class="o"&gt;(&lt;/span&gt;priceData &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
           document.getElementById&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"priceLoader"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;.style.display &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"none"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
           &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;priceData&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
             document.getElementById&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"price"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;.value &lt;span class="o"&gt;=&lt;/span&gt; priceData.price.toFixed&lt;span class="o"&gt;(&lt;/span&gt;2&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
           &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
             alert&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Could not fetch live price."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
           &lt;span class="o"&gt;}&lt;/span&gt;
         &lt;span class="o"&gt;})&lt;/span&gt;
         .getLiveStockPrice&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
     &lt;span class="o"&gt;})&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;


     // Handle form submission
     document.getElementById&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"tradeForm"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;.addEventListener&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"submit"&lt;/span&gt;, &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;e&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
       e.preventDefault&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
       document.getElementById&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"submitBtn"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;.disabled &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
       document.getElementById&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;.style.display &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"block"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
       document.getElementById&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;.className &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"alert alert-info"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
       document.getElementById&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;.innerText &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Placing order..."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;


       google.script.run
         .withSuccessHandler&lt;span class="o"&gt;(&lt;/span&gt;response &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
           document.getElementById&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;.innerText &lt;span class="o"&gt;=&lt;/span&gt; response&lt;span class="p"&gt;;&lt;/span&gt;
           &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;response.includes&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Success"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
             document.getElementById&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;.className &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"alert alert-success"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
             setTimeout&lt;span class="o"&gt;(&lt;/span&gt;google.script.host.close, 3000&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; // Close dialog on success
           &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
             document.getElementById&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;.className &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"alert alert-danger"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
             document.getElementById&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"submitBtn"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;.disabled &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
           &lt;span class="o"&gt;}&lt;/span&gt;
         &lt;span class="o"&gt;})&lt;/span&gt;
         .placeTradeOrder&lt;span class="o"&gt;(&lt;/span&gt;this&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
     &lt;span class="o"&gt;})&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &amp;lt;/script&amp;gt;
 &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: How to Set It All Up
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Update Config.gs&lt;/strong&gt;: Fill in all your personal details in the Config.gs file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store Your API Key&lt;/strong&gt;: In Secrets.gs, paste your API key from Financial Modeling Prep. Then, from the script editor, select the storeApiKey function from the dropdown menu and click Run. You only need to do this once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set Up Triggers&lt;/strong&gt;:&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;In the script editor, go to the Triggers tab (clock icon).&lt;/li&gt;
&lt;li&gt;Click + Add Trigger.&lt;/li&gt;
&lt;li&gt;Choose function to run: processAllEmails.&lt;/li&gt;
&lt;li&gt;Select event source: Time-driven.&lt;/li&gt;
&lt;li&gt;Select type: Minutes timer.&lt;/li&gt;
&lt;li&gt;Select interval: Every 10 minutes.&lt;/li&gt;
&lt;li&gt;Click Save. You will be asked to authorize the script.&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prepare Your Trading Sheet&lt;/strong&gt;: In the Trading tab of your spreadsheet, set up two columns:&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Column A: Month (e.g., "August 2025", "September 2025")&lt;/li&gt;
&lt;li&gt;Column B: Payslip Received (Format this column as checkboxes via Insert &amp;gt; Checkbox)&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prepare Your Transactions Sheet&lt;/strong&gt;: In the Transactions tab, create these headers:Date, Ticker, Type, Quantity, Price, Status&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reload the Spreadsheet&lt;/strong&gt;: Refresh your Google Sheet. You should now see a new "Stock Trading" menu at the top.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4: Testing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Scenario 1: The Automated Payslip Workflow&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;This demonstrates the script's ability to react to incoming emails, save attachments, perform API calls, and update you.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;How to Test / Stimulate It:&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;The script is looking for a new, unread email that matches the criteria in your Config.gs file. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you receive your payslip in outlook like I do and are wondering how to set this up, create an outlook rule to always forward the email with your payslip to your personal gmail account.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To test this, you need to simulate receiving a payslip:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Important&lt;/strong&gt;: For testing, temporarily change the PAYSLIP_SENDER in your Config.gs file to your own email address (e.g., const PAYSLIP_SENDER = "&lt;a href="mailto:your_email@example.com"&gt;your_email@example.com&lt;/a&gt;";).&lt;/li&gt;
&lt;li&gt;From that same email address, send a new email to yourself.&lt;/li&gt;
&lt;li&gt;Subject Line: The subject must contain the phrase "Your Monthly Payslip".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attachment&lt;/strong&gt;: Attach any PDF file to the email.&lt;/li&gt;
&lt;li&gt;Send the email. Once it arrives in your inbox, make sure it is marked as unread.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;What the Script Does (The Demo):&lt;/strong&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Once you've sent the email, you can either wait for the 10-minute trigger to fire or manually run the processAllEmails function from the script editor to see the results immediately.&lt;/li&gt;
&lt;li&gt;The script will find your unread "payslip" email.&lt;/li&gt;
&lt;li&gt;It will save the PDF attachment to the Google Drive folder you specified.&lt;/li&gt;
&lt;li&gt;It will make an API call to get the latest price for AAPL.&lt;/li&gt;
&lt;li&gt;It will calculate how many shares you can buy with 20% of your $10,000 salary.&lt;/li&gt;
&lt;li&gt;It will find the current month in your "Trading" sheet and check the box in the "Payslip Received" column.&lt;/li&gt;
&lt;li&gt;Finally, it will mark the payslip email as read.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;What You Receive / See:&lt;/strong&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;You'll get a new email with the subject "✅ Your Payslip Has Been Processed" containing the stock price information.&lt;/li&gt;
&lt;li&gt;The PDF attachment will appear in your designated Google Drive folder.&lt;/li&gt;
&lt;li&gt;The checkbox for the current month in your "Trading" sheet will be checked.&lt;/li&gt;
&lt;/ul&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%2F28twd9rihemwmv6tmyjm.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%2F28twd9rihemwmv6tmyjm.png" alt="Payslip saving email" width="649" height="236"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Scenario 2: The Manual Stock Trading Workflow&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;This demonstrates the custom user interface you built into the spreadsheet for placing trades.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;How to Test / Stimulate It:&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;This workflow is initiated manually by you.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open your "My Stock Portfolio" Google Sheet.&lt;/li&gt;
&lt;li&gt;A new menu item named "Stock Trading" should appear at the top.&lt;/li&gt;
&lt;/ul&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%2Fzahdh61sa7po3scytj67.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%2Fzahdh61sa7po3scytj67.png" alt="Google sheet menu with the custom menu item Stock trading" width="777" height="139"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Click on Stock Trading &amp;gt; 📈 Place New Trade Order.&lt;/li&gt;
&lt;/ul&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%2F6xbamqvq03xu1oczkmic.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%2F6xbamqvq03xu1oczkmic.png" alt="Stock Trading modal in google sheet" width="472" height="566"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;What the Script Does (The Demo):&lt;/strong&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;A custom dialog box titled "Place a Stock Trade Order" will appear.&lt;/li&gt;
&lt;li&gt;You can click the "Get Live Price" button to have the script fetch and populate the current AAPL stock price.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fill out the form&lt;/strong&gt;: choose Buy or Sell, and enter a Quantity.&lt;/li&gt;
&lt;li&gt;Click the "Place Order" button.&lt;/li&gt;
&lt;li&gt;The script will compose an email with all the trade details and send it to your broker's email address.&lt;/li&gt;
&lt;li&gt;It will BCC you on that email.&lt;/li&gt;
&lt;li&gt;It will add a new row to your "Transactions" sheet to log that the order has been placed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;What You Receive / See:&lt;/strong&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;A confirmation message will appear in the dialog box.&lt;/li&gt;
&lt;/ul&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%2Fh0mivukdylm62crwe4dv.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%2Fh0mivukdylm62crwe4dv.png" alt="Stock Trading modal in google sheet showing order has been placed" width="472" height="566"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You will receive a BCC'd copy of the order email in your inbox.&lt;/li&gt;
&lt;/ul&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%2Fhw8j5cyu8p4cs5nvlmgx.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%2Fhw8j5cyu8p4cs5nvlmgx.png" alt="Email placing the buy order" width="738" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A new row will be added to the "Transactions" sheet.&lt;/li&gt;
&lt;/ul&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%2Ffqu9ojhnutwqd97phgth.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%2Ffqu9ojhnutwqd97phgth.png" alt="Google sheet with updated details on the buy order" width="777" height="139"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Scenario 3: The Automated Contract Note Workflow&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;This demonstrates how the script handles incoming trade confirmations.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;How to Test / Stimulate It:&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Similar to the payslip, you need to simulate receiving a contract note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Change the CONTRACT_NOTE_SENDER in your Config.gs file to your own email address for the test.&lt;/li&gt;
&lt;li&gt;Send a new email to yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subject Line&lt;/strong&gt;: The subject must contain the phrase "Contract Note".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attachment&lt;/strong&gt;: Attach any PDF file.&lt;/li&gt;
&lt;li&gt;Send the email and ensure it's unread.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;What the Script Does (The Demo):&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;When the processAllEmails function runs, it will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Find your unread "Contract Note" email.&lt;/li&gt;
&lt;li&gt;Save the PDF attachment to your Google Drive folder.&lt;/li&gt;
&lt;li&gt;Because the script cannot read the PDF's contents, it will send you a notification email.&lt;/li&gt;
&lt;li&gt;It will mark the contract note email as read.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;What You Receive / See:&lt;/strong&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;You'll get a new email with the subject "📝 Action Required: Log Your Recent Trade", prompting you to manually update your "Transactions" sheet with the final details from the PDF.&lt;/li&gt;
&lt;/ul&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%2F7063hytwqihphg7ef849.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%2F7063hytwqihphg7ef849.png" alt="Contract note saving confirmation email" width="800" height="193"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The contract note PDF will be saved in your Google Drive folder.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Step 5: GitHub Integration with clasp&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;*Feel free to skip this step if you are not technical.&lt;br&gt;
clasp is a command-line tool that lets you manage your Apps Script projects locally and push/pull them to/from GitHub.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install Node.js&lt;/strong&gt;: If you don't have it, install Node.js from nodejs.org.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install clasp&lt;/strong&gt;: Open your terminal or command prompt and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @google/clasp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Login to Google:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;clasp login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will open a browser window for you to authorize clasp.&lt;br&gt;
Enable the Apps Script API: Go to the Apps Script API page and turn it on.&lt;br&gt;
&lt;strong&gt;Clone Your Project:&lt;/strong&gt;&lt;br&gt;
In your Apps Script editor, go to Project Settings (gear icon) and copy the Script ID.&lt;br&gt;
In your terminal, navigate to your desired folder (e.g., cd Documents/GitHub) and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;clasp clone &lt;span class="s2"&gt;"YOUR_SCRIPT_ID_HERE"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will download all your .gs and .html files into a new folder.&lt;br&gt;
&lt;strong&gt;Work with GitHub&lt;/strong&gt;: You can now treat this folder as a standard Git repository.&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;your-project-name
git init
git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Initial commit of finance automation script"&lt;/span&gt;
&lt;span class="c"&gt;# Add your remote and push to GitHub&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pushing Changes Back to Apps Script&lt;/strong&gt;: After making changes locally, just run clasp push.&lt;br&gt;
This setup provides a powerful, automated workflow for managing your finances, all orchestrated from within your Google Workspace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6 (Bonus): Visualization using Looker Studio
&lt;/h2&gt;

&lt;p&gt;You can also take your stock tracking to the next level by building a portfolio dashboard using Looker Studio, with the “My Stock Portfolio” Google Sheet as the data source. This dashboard can display key metrics such as total value bought and sold over time, monthly performance, and even stock-specific trends. By connecting your sheet directly to Looker Studio and visualizing your data through bar charts, line graphs, or scorecards, you gain a real-time, interactive view of your portfolio’s evolution. It’s a great way to stay informed and make data-driven investment decisions.&lt;/p&gt;

&lt;p&gt;The Apps script, however, cannot automate the making of the charts and so you will need to add, format and align them manually.&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%2Frfi9l3papn6r45knky5v.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%2Frfi9l3papn6r45knky5v.png" alt="Investment portfolio dashboard on looker studio" width="800" height="404"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;And there you have it! A simple project based introduction to Google Apps Script. &lt;/p&gt;

&lt;p&gt;We’ve covered how to set up triggers, interact with Gmail, parse attachments, and store secrets securely—while also touching on important limitations. The biggest takeaway? You don’t need external tools to start automating tasks right inside your Google Workspace—Apps Script gives you a surprisingly powerful head start.&lt;br&gt;
I am curious what you choose to automate first. Let me know in the comments. Also, let me know if I should deploy the automated stock ordering custom menu as a google sheet add on. It’s definitely a time saver for me.&lt;/p&gt;

&lt;p&gt;I will get to n8n and Zapier in due time but for now, Google Apps Script serves me well. Till next time, have fun.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>googleworkspace</category>
      <category>googleappsscript</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Frisque – Using AI agents for Due Diligence</title>
      <dc:creator>Benson King'ori</dc:creator>
      <pubDate>Mon, 23 Jun 2025 17:40:20 +0000</pubDate>
      <link>https://dev.to/virgoalpha/frisque-using-ai-agents-for-due-diligence-4old</link>
      <guid>https://dev.to/virgoalpha/frisque-using-ai-agents-for-due-diligence-4old</guid>
      <description>&lt;h2&gt;
  
  
  TLDR;
&lt;/h2&gt;

&lt;p&gt;Frisque uses django, ai agents, celery and rabbitmq to automate due diligence on startups. It takes text, pitch decks, financial spreadsheets and even video as input and outputs &lt;a href="https://drive.google.com/file/d/1jO7UjrZn8zDurIlp8sPRDpUQgXQkG3mu/view?usp=sharing" rel="noopener noreferrer"&gt;an investment memo&lt;/a&gt;. It was built for the Agent Development Kit Hackathon with Google Cloud on DevPost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;li&gt;The Problem&lt;/li&gt;
&lt;li&gt;Our Solution&lt;/li&gt;
&lt;li&gt;Architecture and Stack&lt;/li&gt;
&lt;li&gt;Agentic AI – Orchestration in Action&lt;/li&gt;
&lt;li&gt;Challenges and Revelations&lt;/li&gt;
&lt;li&gt;Key Learnings&lt;/li&gt;
&lt;li&gt;Future Plans&lt;/li&gt;
&lt;li&gt;DYI&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In the dynamic world of Venture Capital (VC), conducting due diligence on potential startup investments is a critical yet cumbersome bottleneck. This process became painfully familiar through firsthand experience interning in VC, revealing the sheer volume of information, meticulous cross-referencing, and relentless pressure to identify both opportunity and risk within tight timeframes. Frisque was born directly from this experience, aiming to automate and augment these very tasks and challenges.&lt;/p&gt;

&lt;p&gt;VC firms inherently dedicate significant time and resources to due diligence, as it is crucial for assessing a startup's viability and growth potential. Given that a fund's returns often originate from a small percentage of its investments, streamlining this process is absolutely crucial for success. Frisque aims to go beyond mere efficiency, enabling VCs to make smarter, faster, and more informed investment decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Having both spent time interning in the dynamic world of Venture Capital, we quickly became painfully familiar with a critical, yet cumbersome, bottleneck: &lt;em&gt;due diligence&lt;/em&gt;. The sheer volume of information, the meticulous cross-referencing, and the relentless pressure to identify both opportunity and risk within a tight timeframe becomes incredibly apparent in such roles. Frisque was born out of this firsthand experience, directly addressing the very tasks and challenges we faced, aiming to automate and augment the work we were doing.&lt;/p&gt;

&lt;p&gt;Venture Capital (VC) firms dedicate a significant amount of time and resources to conducting due diligence on potential startup investments. This process is critical for assessing a startup's viability and growth potential, but it is incredibly time-intensive. Given that a fund's returns often come from a small percentage of its investments (pareto principle), streamlining this process is absolutely crucial for success. It's about more than just efficiency; it's about making smarter, faster, and more informed investment decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Solution
&lt;/h2&gt;

&lt;p&gt;Frisque, aptly named from the French "Faux Risque" (false risk), is an AI-powered platform built to revolutionize the VC due diligence process by significantly reducing the time and effort VCs spend on initial assessments while dramatically improving the depth and breadth of insights.&lt;br&gt;
At its core, Frisque is a web-based platform built on Django, uniquely leveraging Google's open-source Agent Development Kit (ADK) to create a sophisticated multi-agent AI system. This approach means Frisque isn't just one large AI, but a coordinated team of specialized AI "agents" working together, mirroring a human due diligence team.&lt;br&gt;
Here's how Frisque's agentic system streamlines the process:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Comprehensive Input Collection&lt;/em&gt;: Analysts can initiate "scans" on target companies. They provide a wide array of inputs, including company names, website URLs, business plans, pitch decks, lean canvases, founder profiles, social media links, and even financial documents (like spreadsheets) and government registration documents. The system also allows users to select which specific types of scans they want to perform, such as Tech, Legal, or Financial analysis.&lt;br&gt;
&lt;em&gt;Intelligent Agent Orchestration&lt;/em&gt;: Once a scan is initiated, a Master Bot, or Orchestrator Agent, takes charge. It intelligently delegates specific sub-tasks to a team of specialized worker agents. This multi-agent by design approach is a core strength of Google's ADK, enabling complex coordination and task delegation.&lt;/p&gt;

&lt;p&gt;Specialized Agents in Action:&lt;br&gt;
The Tech Bot assesses a startup's technology stack, scalability, and potentially intellectual property.&lt;br&gt;
The Legal Bot sifts through provided legal documents to identify basic red flags or critical phrases in contracts and registrations.&lt;br&gt;
The Market Research Bot gathers crucial data on market size, industry trends, and competitor landscapes.&lt;br&gt;
The Social Media Sentiment Bot analyzes public sentiment around the company and its founders from various social media profiles.&lt;br&gt;
The Financial Bot performs basic analysis of financial statements, capable of detecting anomalies or inconsistencies in the data.&lt;/p&gt;

&lt;p&gt;These agents utilize Large Language Models (LLMs), Natural Language Processing (NLP) tools, and can integrate with external APIs or custom tools as needed. A key learning was that ADK's inherent agency allows bots to choose their own tools, meaning we didn't need to explicitly direct them, which streamlined our development. We also adopted a "Pipeline / Assembly line architecture" or "Dumb Worker, Smart Master" pattern, where complex logic is handled by the master agent and a dedicated worker agent formats responses, effectively solving issues like prompt leakage and hallucination we initially encountered. This approach reinforces the benefits of a microservices design over a monolithic one for scalability and isolation.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Comprehensive Output Generation&lt;/em&gt;: The system synthesizes the findings from all the specialized agents into a comprehensive and actionable suite of outputs. This includes a structured Investment Memo (a go/no-go document), a summary Dashboard with key findings and scores, and if financial data is provided, basic financial projections. It also provides a valuable list of assumptions made, key questions to ask the startup, and all cited sources.&lt;br&gt;
&lt;em&gt;Real-time Updates and Notifications&lt;/em&gt;: To keep analysts informed throughout the process, Frisque provides real-time updates on scan progress directly on the results page using Django Channels (WebSockets). Users also receive both in-app notifications and email notifications once a scan is complete.&lt;/p&gt;

&lt;p&gt;By leveraging Google's ADK and a modern stack including Django, PostgreSQL, Celery, RabbitMQ, and Google Cloud services like Google Cloud Storage and Vertex AI, Frisque is designed to be modular, scalable, and deployment-ready. This project is also a contribution to the Agent Development Kit Hackathon with Google Cloud, highlighting our use of Google Cloud technologies and the open-source ADK&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture and Stack
&lt;/h2&gt;

&lt;p&gt;Frisque's architecture and technology stack are designed for modularity, scalability, and efficient AI-powered due diligence. It aims to support asynchronous workloads and intelligent processing.&lt;br&gt;
Here's a breakdown of the key components:&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%2Fw21uqhepoqp4bb7506vy.jpg" 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%2Fw21uqhepoqp4bb7506vy.jpg" alt="Our Architecture diagram" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Backend Framework (Django)&lt;/em&gt;: Frisque is a web-based platform built on Django. Django provides a self-contained framework for the application's backend. Its ORM (Object-Relational Mapper) simplifies database interactions by managing models for users, companies, and scan jobs. This structure allows for quick integration with other technologies, such as Docker, for consistent development and deployment.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AI Agents (Google Agent Development Kit - ADK)&lt;/em&gt;: The platform leverages Google's Agent Development Kit (ADK) to create its multi-agent AI system. ADK is an open-source, code-first framework designed for building and deploying sophisticated AI agents. It is "Multi-Agent by Design," enabling complex coordination and delegation of tasks within a team of agents. The Agent Starter Pack provides an easier way to quickly set up, customize, and deploy agents. This approach supports modular and scalable development, breaking down intricate problems into manageable sub-tasks handled by specialized agents.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Asynchronous Task Queue (Celery) and Message Broker (RabbitMQ)&lt;/em&gt;: Frisque uses Celery for background task processing. When a scan is initiated, a Celery task is dispatched to handle it asynchronously. This allows for scheduling and managing complex, time-consuming operations outside of the main web request flow. RabbitMQ (or Redis) serves as the message broker for Celery, facilitating communication between the application and the worker processes.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Containerization (Docker and Docker Compose)&lt;/em&gt;: Docker is used for containerization, ensuring that the application and all its dependencies are packaged into isolated units. Docker Compose simplifies the management of multi-container Docker applications for local development. This setup provides reproducibility across different environments, making it easy to get the development environment up and running consistently. All development commands are designed to be run inside the web container for a consistent workflow.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Database (PostgreSQL)&lt;/em&gt;: PostgreSQL is the chosen database for storing structured data. This includes details of target companies and scan job metadata.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Object Storage (Google Cloud Storage - GCS)&lt;/em&gt;: Google Cloud Storage (GCS) is integrated for storing unstructured data. This includes uploaded documents like pitch decks and financial spreadsheets, as well as generated reports and memos.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Real-time Communication (Django Channels)&lt;/em&gt;: Django Channels, utilizing WebSockets, enables real-time updates and notifications. This allows the scan results page to display live progress updates and provides in-app notifications upon scan completion.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Infrastructure as Code (Terraform)&lt;/em&gt;: Terraform is used for provisioning Google Cloud Platform (GCP) resources. This ensures that the cloud infrastructure is managed consistently and repeatably.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cloud Platform (Google Cloud Platform - GCP)&lt;/em&gt;: The entire system is designed to leverage Google Cloud Platform services for deployment and scalability. This includes potential use of Vertex AI for Agent Engine and LLM hosting, Cloud Run for serverless agent deployment, and Cloud SQL for managed PostgreSQL. Frisque is also a contribution to the Agent Development Kit Hackathon with Google Cloud.&lt;/p&gt;

&lt;p&gt;This comprehensive stack allows Frisque to efficiently process complex due diligence tasks, manage data, and provide real-time insights to users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agentic AI – Orchestration in action
&lt;/h2&gt;

&lt;p&gt;Frisque's power lies in its agentic AI system, meticulously designed to replicate and enhance the collaborative nature of a human due diligence team. This sophisticated structure is made possible by leveraging Google's open-source Agent Development Kit (ADK), a framework built to develop, evaluate, and deploy sophisticated AI agents and multi-agent systems. ADK is inherently "Multi-Agent by Design," which means it excels at enabling complex coordination and delegation of tasks within a hierarchy or team of agents.&lt;br&gt;
When an analyst initiates a "scan" on a target company within Frisque, a comprehensive process of intelligent orchestration begins.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Orchestrator Agent (Master Bot)&lt;/em&gt;: At the core of this system is a Master Bot, acting as the Orchestrator Agent. Its primary role is to receive the initial scan request and intelligently delegate specific sub-tasks to a team of specialized worker agents. This delegation is crucial for breaking down intricate due diligence problems into manageable parts.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Specialized Agents in a Pipeline&lt;/em&gt;: Frisque employs a diverse set of specialized worker agents, each with a distinct focus. These include the Tech Bot, Legal Bot, Market Research Bot, Social Media Sentiment Bot, and Financial Bot. A key learning during development was the adoption of a "Pipeline / Assembly line architecture" or "Dumb Worker, Smart Master" pattern. In this architecture, the complex logic and coordination are handled by the master agent, while a dedicated worker agent is specifically responsible for formatting the responses. This separation of concerns proved vital in solving initial challenges like prompt leakage and hallucination, reinforcing the benefits of a microservices design for scalability and isolation.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Intelligent Tool Selection and Inquiry&lt;/em&gt;: A significant aspect of the agents' intelligence lies in their inherent agency. The ADK's design allows bots to choose their own tools without explicit direction from the developer. This means agents can intelligently decide which resources to use for their tasks, whether it's utilizing Large Language Models (LLMs), Natural Language Processing (NLP) tools, integrating with external APIs, or even using other agents as tools. This self-directed tool selection, and the ability to inquire further after obtaining initial results, streamlines the development process and enhances the depth of research. For instance, the Market Research Bot might autonomously decide to use web search tools to gather market size data or a sentiment API to analyze social media.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Synthesis and Output&lt;/em&gt;: As each specialized agent completes its analysis, it gathers, processes, and analyzes information based on its function and the provided inputs. The Orchestrator then synthesizes these findings from all the specialized agents into comprehensive and actionable outputs. This culminates in a structured Investment Memo, a summary Dashboard with key findings and scores, and potentially basic financial projections. The system also provides a valuable list of assumptions made, key questions to ask the startup, and all cited sources.&lt;/p&gt;

&lt;p&gt;This orchestrative, multi-agent approach allows Frisque to efficiently process complex due diligence tasks, manage vast amounts of data, and provide real-time, insightful analyses to VC firms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges and Revelations
&lt;/h2&gt;

&lt;p&gt;One of the first and most critical challenges was agent hallucination. Agents were generating incorrect or fabricated information. Closely related was prompt leakage. This occurred due to difficulties in system integration. Initially, the master agent was responsible for both task delegation and response formatting. This design inadvertently led to the agents' tendency to hallucinate and expose prompts in unintended ways.&lt;/p&gt;

&lt;p&gt;We fixed the above by creating a new agent to format the final output before it is returned by the master agent. Even in this we had to be explicit in terms of the fields we required in the output in both the master agent and the formatting agent. Otherwise we would get errors of missing fields.&lt;/p&gt;

&lt;p&gt;Another inherent challenge in building multi-agent systems, particularly with complex interactions, involves designing and debugging their orchestration. Ensuring the consistency and accuracy of Large Language Model (LLM) calls across various agent tasks, while also managing their associated costs, proved challenging. The overall quality and availability of input data for target companies also directly impacted the effectiveness of the agents. Finally, effectively quantifying and training agents to achieve the depth of insight expected by experienced Venture Capitalists was a significant undertaking&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Learnings
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Embracing a Pipeline/Microservices Architecture for Agentic Systems: Our development journey revealed that complex multi-agent systems, especially those dealing with detailed outputs, can suffer from agent hallucination and prompt leakage. This was a profound revelation, teaching us the crucial importance of a pipeline or assembly line architecture. By introducing a new, dedicated agent solely for formatting the final output, we achieved a clearer separation of concerns. This "Dumb Worker, Smart Master" pattern proved far more effective for managing complex logic, allowing agents to specialize. This experience solidified our conviction that a microservices approach is generally superior to a monolith for deployment, offering benefits like enhanced scalability, technology flexibility, reduced single points of failure, and faster deployments. We saw how individual management of agents became possible, allowing us to explore other technologies if needed.&lt;/li&gt;
&lt;li&gt;Leveraging Google Agent Development Kit (ADK) for Multi-Agent Orchestration: ADK, as an open-source, code-first framework, became the backbone of Frisque, empowering us to build, evaluate, and deploy sophisticated AI agents. Its "Multi-Agent by Design" principle was instrumental for enabling the complex coordination and task delegation within our system. We learned to fully utilize ADK's flexible orchestration capabilities, including both workflow agents for predictable pipelines (like SequentialAgent, ParallelAgent, and LoopAgent) and LLM-Driven Dynamic Routing for adaptive behaviors. The integrated developer experience, complete with a command-line interface (CLI) and a visual Web UI, significantly aided our development, allowing us to run agents, inspect execution steps, and debug interactions in real-time. The built-in observability and debugging tools, which log agent decisions, tool usage, and trace delegation paths, were invaluable for understanding and refining our agents' behavior.&lt;/li&gt;
&lt;li&gt;Precision in Prompting and Agent Tooling: A critical insight gained was that agents within ADK do not need explicit direction on which tools to use. Simply providing the prompt is sufficient, as ADK already exposes the available tools to the agent, and the agent's selection of tools is part of its inherent agency. However, we also learned the critical importance of being explicit in terms of required output fields (both in the master agent's instructions and the formatting agent's directives) to prevent errors and ensure consistent data.&lt;/li&gt;
&lt;li&gt;The Paramount Importance of Data Quality: The effectiveness of our AI agents in due diligence directly correlated with the quality and availability of input data. This highlighted the absolute necessity of establishing robust data management processes and infrastructure for input collection and storage. We chose Google Cloud Storage (GCS) for securely housing uploaded documents and generated reports, with PostgreSQL maintaining structured data and references.&lt;/li&gt;
&lt;li&gt;Balancing AI Capabilities with Human Expectation: Quantifying and training agents to achieve the depth of insight expected by experienced Venture Capitalists proved a significant undertaking. Our learning here was the value of an iterative and specialized approach. By developing distinct agents for different domains—such as Tech Bot, Legal Bot, Market Research Bot, Social Media Sentiment Bot, and Financial Bot—we could address specific analytical tasks. This modularity, combined with a basic scoring mechanism, is our path to incrementally achieving sophisticated VC-level insights, understanding that AI augments, rather than replaces, human judgment in complex financial decisions&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Future Plans
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Collapse the results into a downloadable pdf document&lt;/li&gt;
&lt;li&gt;Add more ai agents&lt;/li&gt;
&lt;li&gt;Add email notification for when a scan is done&lt;/li&gt;
&lt;li&gt;Allow selective scans, e.g., security, sentiment analysis, social media, legal, etc&lt;/li&gt;
&lt;li&gt;Create a scan history and dashboard pages&lt;/li&gt;
&lt;li&gt;Integrate MCP, A2A and other integrations&lt;/li&gt;
&lt;li&gt;Scoring of startups for investments purposes&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  DYI
&lt;/h2&gt;

&lt;p&gt;Please find the code &lt;a href="https://github.com/Virgo-Alpha/Frisque" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;br&gt;
Follow the results in the README to reproduce the project. No api keys or envs needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The comprehensive technology stack employed by Frisque allows it to efficiently process complex due diligence tasks, manage data, and provide real-time insights to users. The process of due diligence mirrors that of fundraising. Marc Andressen once compared fundraising rounds to removing the layers of an onion and we hope Frisque can help make this less tear-worthy for VCs.&lt;/p&gt;

</description>
      <category>adk</category>
      <category>adkhackathon</category>
      <category>gcp</category>
      <category>ai</category>
    </item>
    <item>
      <title>Data Engineering Concepts: A project based introduction</title>
      <dc:creator>Benson King'ori</dc:creator>
      <pubDate>Wed, 14 May 2025 09:14:41 +0000</pubDate>
      <link>https://dev.to/virgoalpha/data-engineering-concepts-a-project-based-introduction-4pka</link>
      <guid>https://dev.to/virgoalpha/data-engineering-concepts-a-project-based-introduction-4pka</guid>
      <description>&lt;p&gt;I recently finished the &lt;a href="https://github.com/DataTalksClub/data-engineering-zoomcamp" rel="noopener noreferrer"&gt;Data Engineering Zoomcamp&lt;/a&gt; by &lt;a href="https://datatalks.club/" rel="noopener noreferrer"&gt;DataTalks Club&lt;/a&gt;. For my certification, I was required to undertake a capstone project that would culminate in a dashboard showing insights from the data I had processed in my pipeline.&lt;/p&gt;

&lt;p&gt;Instead of a step-by-step guide (which can be easily found in the project’s &lt;a href="https://github.com/Virgo-Alpha/LinkedIn_Job_Posts_Insights" rel="noopener noreferrer"&gt;README&lt;/a&gt;), this article explores &lt;strong&gt;data engineering concepts from a high-level view&lt;/strong&gt;, explaining the decisions I made and the trade-offs I considered.&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%2F5nysrnixaiuh2inf8x3q.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%2F5nysrnixaiuh2inf8x3q.png" alt="My Project chart" width="800" height="613"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Data Sourcing and Problem Definition&lt;/li&gt;
&lt;li&gt;Containerization&lt;/li&gt;
&lt;li&gt;Infrastructure-as-Code (IaC)&lt;/li&gt;
&lt;li&gt;Orchestration vs Automation&lt;/li&gt;
&lt;li&gt;Data Lake and Data Warehouse&lt;/li&gt;
&lt;li&gt;Analytics Engineering and Data Modeling&lt;/li&gt;
&lt;li&gt;Batch vs Streaming&lt;/li&gt;
&lt;li&gt;Exposure: Visualization and Predictions&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. Data Sourcing and Problem Definition
&lt;/h2&gt;

&lt;p&gt;Before coding, I sourced the data and defined the problem. I chose the &lt;strong&gt;LinkedIn Job Postings dataset from Kaggle&lt;/strong&gt; due to its richness and descriptive documentation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem Statement:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;How can data from LinkedIn job posts (2023–2024) help us make informed decisions on a career path?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I then went ahead to break it down into the following issues, each of which would be addressed by a chart in my dashboard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which job titles offer the highest salaries?&lt;/li&gt;
&lt;li&gt;Which companies, industries, and skills are the most lucrative?&lt;/li&gt;
&lt;li&gt;What percentage of companies offer remote work?&lt;/li&gt;
&lt;li&gt;What are the highest salaries and average experience levels?&lt;/li&gt;
&lt;li&gt;Which countries have the most job postings?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These were, of course, not MECE-compliant. (MECE - mutually exclusive and collectively exhaustive)&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Containerization
&lt;/h2&gt;

&lt;p&gt;Before I could think of extracting my data, I took a high level and long term view of my project and considered aspects such as collaboration and reproducibility. Whereas it is true that I could easily just create the pipelines and files needed on my local machine with no packaging whatsoever, this would pose a challenge to anyone who would be looking to evaluate or reproduce my project. I, hence, decided to use docker containers to package my project for replication either locally or even in the cloud. &lt;/p&gt;

&lt;p&gt;Docker containers also have other advantages such as being lightweight, easily replicable thus allowing scalability via horizontal scaling and load balancing, increases project maintainability since Dockerfiles simplify environment management, isolation between the containers prevents dependency conflicts and it supports version control via versioned images.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Infrastructure as Code (IaC)
&lt;/h2&gt;

&lt;p&gt;I would be using GCP (Google’s cloud wing) for my data lake, data warehouse and dashboard hosting and so I needed a reliable way to interact with the cloud. Infrastructure-as-Code (IaC) is the practice of managing and provisioning computing infrastructure—such as servers, networks, databases, and other resources—through machine-readable configuration files rather than through manual processes.&lt;/p&gt;

&lt;p&gt;The use of IaC tools simplifies the process of cloud infrastructure management and allows for scalability, version control, testability and automation. Apart from provisioning infrastructure, IaC tools can be used for other management activities such as enabling APIs in GCP and many more. It also allows reusability of resources since it avoids creating new resources if the defined ones already exist.&lt;/p&gt;

&lt;p&gt;Terraform is the IaC tool that I used due to how simple it is. I made it modular and included the use of variables and outputs to integrate terraform into my project’s workflow. An alternative to terraform is AWS Cloudformation which is used in AWS setups.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Orchestration vs Automation
&lt;/h2&gt;

&lt;p&gt;A data workflow is a sequence of automated data processing steps that specifies what are the steps, inputs, outputs, and dependencies in a data processing pipeline. Data workflows are also called DAGs (Directed Acyclic Graphs). Directed means they have direction, Acyclic means there are no cycles. There may be loops but no cycles are allowed. The difference between a loop and a cycle in this case is that in a loop, we know the starting and ending point. The loop ends based on whether a certain condition is met but a cycle has none. DAGs are run using tools / engines for orchestration like Apache Airflow, Luigi, Prefect, Dagster, Kestra, etc. Smaller workflows can be run using make and or cron jobs but this is usually done locally.&lt;/p&gt;

&lt;p&gt;In software engineering and data management, an orchestrator is a tool that automates, manages, and coordinates various workflows and tasks across different services, systems, or applications. Because an orchestrator allows everything to run smoothly without the need for manual intervention, it is easy to confuse orchestration with automation.&lt;/p&gt;

&lt;p&gt;Whereas automation refers to the execution of individual tasks or actions without manual intervention, orchestration goes beyond automation by managing the flow of multiple interconnected tasks or processes. Orchestration defines not only what happens but also when and how things happen, ensuring that all tasks (whether automated or not) are executed in the correct order, with the right dependencies and error handling in place. While automation focuses on individual tasks, orchestration ensures all those tasks are arranged and managed within a broader, cohesive system. This matters if you need to reliably handle complex processes with many interdependent steps.&lt;/p&gt;

&lt;p&gt;Use cases for automation include automated testing after code commits, automated backups and automated email notifications. Use cases for orchestration include data pipeline orchestration, CI/CD pipeline orchestration and cloud infrastructure orchestration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Advantages of workflow orchestration include:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Scalability&lt;/li&gt;
&lt;li&gt;Error handling and resilience&lt;/li&gt;
&lt;li&gt;Improved monitoring and control&lt;/li&gt;
&lt;li&gt;Process standardization&lt;/li&gt;
&lt;li&gt;Faster time to value since no need to reinvent the wheel&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What’s the Difference?
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Automation&lt;/th&gt;
&lt;th&gt;Orchestration&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scope&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Single task execution&lt;/td&gt;
&lt;td&gt;Coordination of multiple tasks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Focus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Efficiency of individual actions&lt;/td&gt;
&lt;td&gt;Dependency management, error handling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Automated backups&lt;/td&gt;
&lt;td&gt;CI/CD pipelines, data pipeline scheduling&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For my workflow orchestration tool, I chose Apache Airflow because it is the most common in the industry. I built Airflow using a docker-compose.yml file and Dockerfile which installs google sdk (a way to interact with GCP). I then created a dag that had multiple steps which include downloading, unzipping and uploading the data to the created gcs bucket.&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%2Fsxmyzd09gk8oo5bbabg1.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%2Fsxmyzd09gk8oo5bbabg1.png" alt="Tasks and steps in my Airflow DAG" width="800" height="191"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I Used Apache Airflow
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Industry standard for DAG orchestration&lt;/li&gt;
&lt;li&gt;Allows complex workflows&lt;/li&gt;
&lt;li&gt;Supports retries, alerts, and dependency management&lt;/li&gt;
&lt;li&gt;Easily containerized using &lt;code&gt;docker-compose.yml&lt;/code&gt; and custom &lt;code&gt;Dockerfile&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  5. Data Lake vs Data Warehouse
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Data Lake
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;data lake&lt;/strong&gt; is a centralized repository that allows you to store structured, semi-structured, and unstructured data at any scale, in its raw, native format until it's needed for analysis.&lt;/p&gt;

&lt;h4&gt;
  
  
  Features of a data lake:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Allows ingestion of structured and unstructured data&lt;/li&gt;
&lt;li&gt;Catalogs and indexes data for analysis without data movement&lt;/li&gt;
&lt;li&gt;Stores, secures and protects data at an unlimited scale&lt;/li&gt;
&lt;li&gt;Connects data with analytics and ML tools&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Why do we need a data lake:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Companies realized the value of data&lt;/li&gt;
&lt;li&gt;Allows for quick storage and access of data (contingent on tier if S3)&lt;/li&gt;
&lt;li&gt;It is hard to always be able to define the structure of data at the onset&lt;/li&gt;
&lt;li&gt;Data usefulness is sometimes realized later in the project lifecycle&lt;/li&gt;
&lt;li&gt;R&amp;amp;D on data products requires huge amounts of data&lt;/li&gt;
&lt;li&gt;The need for cheap storage of Big Data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cloud providers of data lakes include Google Cloud Storage by GCP, S3 by AWS and Azure Blob by Azure.&lt;/p&gt;

&lt;h4&gt;
  
  
  Dangers in a data lake:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Conversion into a data swamp (disorganized, inaccessible, and untrustworthy data lake)&lt;/li&gt;
&lt;li&gt;No versioning&lt;/li&gt;
&lt;li&gt;Incompatible schemas for same data without versioning&lt;/li&gt;
&lt;li&gt;No metadata associated&lt;/li&gt;
&lt;li&gt;Joins not possible&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Data Warehouse
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;data warehouse&lt;/strong&gt; is a centralized, structured repository designed to store, manage, and analyze large volumes of cleaned and organized data from multiple sources to support business intelligence (BI), reporting, and decision-making. This is where we have the partitioning and clustering capabilities.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Data Lake&lt;/th&gt;
&lt;th&gt;Data Warehouse&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Raw (structured + unstructured)&lt;/td&gt;
&lt;td&gt;Refined (structured only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Purpose&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Storage for future use&lt;/td&gt;
&lt;td&gt;Fast analytics &amp;amp; reporting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Design&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Schema-on-read&lt;/td&gt;
&lt;td&gt;Schema-on-write&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Example Tool&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Google Cloud Storage&lt;/td&gt;
&lt;td&gt;BigQuery, Redshift, Snowflake&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Data warehouse cloud providers include GCP BigQuery, Amazon Redshift and Snowflake by Azure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Additional Topics about Data Warehousing
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Unbundling
&lt;/h4&gt;

&lt;p&gt;Data warehouse unbundling is the process of breaking apart a traditional, monolithic data warehouse into distinct, independently scalable components. In practice, this involves decoupling ingestion, storage, processing and compute allowing the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scale the the two independently&lt;/li&gt;
&lt;li&gt;Adopt best-of-breed tools because of modularity leading to better performance, agility and innovation.&lt;/li&gt;
&lt;li&gt;Improve agility and maintenance&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Not all data warehouses are unbundled so be sure to check out if the one you want to use is.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;OLAP vs OLTP&lt;/strong&gt;:
&lt;/h4&gt;

&lt;p&gt;Online analytical processing (OLAP) and online transaction processing (OLTP) are data processing systems that help you store and analyze business data. You can collect and store data from multiple sources—such as websites, applications, smart meters, and internal systems. OLAP combines and groups the data so you can analyze it from different points of view. Conversely, OLTP stores and updates transactional data reliably and efficiently in high volumes. OLTP databases can be one among several data sources for an OLAP system. Both online analytical processing (OLAP) and online transaction processing (OLTP) are database management systems for storing and processing data in large volumes.&lt;/p&gt;

&lt;p&gt;The primary purpose of online analytical processing (OLAP) is to analyze aggregated data, while the primary purpose of online transaction processing (OLTP) is to process database transactions. You use OLAP systems to generate reports, perform complex data analysis, and identify trends. In contrast, you use OLTP systems to process orders, update inventory, and manage customer accounts. A data warehouse is an OLAP solution.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;OLTP&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;OLAP&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Purpose&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manage and process real-time transactions/business operations&lt;/td&gt;
&lt;td&gt;Analyze large volumes of data to support decision-making&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Updates&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Short, fast updates initiated by users&lt;/td&gt;
&lt;td&gt;Data periodically refreshed with scheduled, long-running batch jobs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Database Design&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Normalized databases for efficiency and consistency&lt;/td&gt;
&lt;td&gt;Denormalized databases using star/snowflake schemas for analytical queries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Space Requirements&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Generally small (if historical data is archived)&lt;/td&gt;
&lt;td&gt;Generally large due to aggregating large datasets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Response Time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Milliseconds – optimized for speed&lt;/td&gt;
&lt;td&gt;Seconds or minutes – optimized for complex queries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backup and Recovery&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Frequent backups required for business continuity&lt;/td&gt;
&lt;td&gt;Data can be reloaded from OLTP systems in lieu of regular backups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Productivity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Increases productivity of end-users and transaction handlers&lt;/td&gt;
&lt;td&gt;Increases productivity of analysts, executives, and decision-makers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data View&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Detailed, day-to-day business transactions&lt;/td&gt;
&lt;td&gt;Aggregated, multi-dimensional view of enterprise data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Example Applications&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Order processing, payments, inventory updates&lt;/td&gt;
&lt;td&gt;Trend analysis, forecasting, executive dashboards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;User Examples&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Customer-facing staff, clerks, online shoppers&lt;/td&gt;
&lt;td&gt;Business analysts, data scientists, senior management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Structure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Row-based storage&lt;/td&gt;
&lt;td&gt;Columnar storage (in most modern OLAP systems like BigQuery, Redshift, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Examples by Provider&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Google Cloud SQL, Amazon Aurora, Cloud Spanner&lt;/td&gt;
&lt;td&gt;BigQuery, Amazon Redshift, Snowflake&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  6. Analytics Engineering and Data Modeling
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What Is Analytics Engineering?
&lt;/h3&gt;

&lt;p&gt;Analytics engineering is a field that seeks to bridge the gap between data engineering and data analysis. It introduces good software engineering practices (such as modularity, version control, testing, documentation and DRY) to the efforts of data analysts and data scientists. This is done by the use of tools such as dbt, dataform, aws glue and sqlmesh. &lt;/p&gt;

&lt;h3&gt;
  
  
  Data Modeling:
&lt;/h3&gt;

&lt;p&gt;Data modelling is the process of defining and organizing the structure of data within a system or database to ensure consistency, clarity, and usability. It involves creating abstract representations (models) of how data is stored, connected, and processed. There are three levels of data models:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Conceptual - High-level view of business entities and relationships (dimensions tables)&lt;/li&gt;
&lt;li&gt;Logical - Defines the structure and attributes of data without database-specific constraints. Measurements, metrics or facts (Facts tables)&lt;/li&gt;
&lt;li&gt;Physical - Maps the logical model to actual database schemas, tables, indexes, and storage.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ETL vs ELT
&lt;/h3&gt;

&lt;p&gt;We can either use ELT or ETL when transforming data. The letters represent the same words (Extract, Load, Transform) but the order matters.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;ETL&lt;/th&gt;
&lt;th&gt;ELT&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Volume&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Small&lt;/td&gt;
&lt;td&gt;Large&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Transformation Time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Before loading&lt;/td&gt;
&lt;td&gt;After loading&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Flexibility&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lower&lt;/td&gt;
&lt;td&gt;Higher&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Higher compute&lt;/td&gt;
&lt;td&gt;Lower overall&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In terms of data modelling, I used cloud dbt because it is easy to use and allows for integration with Github. It does have a limit of one dbt project for the non-premium account so I did have to delete a previous project.&lt;/p&gt;

&lt;p&gt;Instead of having data duplication by uploading all my data from the GCS bucket into BigQuery, I used dbt to create external tables which reference data without having to load it into BigQuery’s native storage. The trade off here is that performance in areas such as access and querying may be a little slow due to on-the-fly reading.&lt;/p&gt;

&lt;p&gt;I also used dbt to partition and cluster my tables in bigQuery. Partitioning splits a table into segments (partitions) based on the values of a specific column (usually date/time or integer range). Each partition stores a subset of the table’s data, and queries can skip entire partitions that aren’t relevant. The same query processes less data for a partitioned table than for a non-partitioned table thus saving both time and money for queries that are frequently run. You can have a maximum of 4000 partitions in a table. &lt;/p&gt;

&lt;p&gt;Clustering, on the other hand, organizes rows within a partition (or unpartitioned table) based on the values in one or more columns. It enables fine-grained pruning of data during query execution and optimizes filter operations, joins and aggregations by organizing data on disk. You can specify up to 4 columns to cluster by; They must be top level and non-repeated fields. Big query performs automatic reclustering for newly added data at no cost.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Partitioning&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Clustering&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Granularity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Coarse (splits table into partitions)&lt;/td&gt;
&lt;td&gt;Fine (organizes rows within partitions/tables)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Basis&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One column (DATE/TIMESTAMP/INTEGER)&lt;/td&gt;
&lt;td&gt;Up to 4 columns (any type)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Skips entire partitions&lt;/td&gt;
&lt;td&gt;Skips blocks of rows within a table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost Efficiency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reduces scan by entire partitions. Cost is predictable&lt;/td&gt;
&lt;td&gt;Reduces scan via pruning but cost benefit varies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Storage Layout&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Logical partitioning (physically separated partitions)&lt;/td&gt;
&lt;td&gt;Physical sorting within storage blocks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best Used For&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Time-series or log data&lt;/td&gt;
&lt;td&gt;Frequently filtered or grouped columns with repetition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Limitations&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Max 4000 partitions per table&lt;/td&gt;
&lt;td&gt;Max 4 clustering columns; no nested/repeated fields&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  7. Batch vs Streaming
&lt;/h2&gt;

&lt;p&gt;In data engineering and big data, there is usually large amounts of data being generated all the time. A data engineer will thus need to decide whether to process the data as it comes (streaming) or batch it up and process it as intervals.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to decide between streaming and batch
&lt;/h3&gt;

&lt;p&gt;A good heuristic to follow is to only use streaming when there is an automated response to the data at the end of the pipeline instead of just a human analyst looking at the data (that'd be over engineering). As such, use cases for streaming include fraud detection, hacked account detection and surge pricing (uber).&lt;/p&gt;

&lt;p&gt;Batch is best for use cases where data is generated in large volumes but not continuously and can be processed in intervals. You can even use micro batching (15 and 60 minute batches) in case you have a lot of data but not enough to justify streaming.&lt;/p&gt;

&lt;p&gt;Streaming uses a pub-sub model where publishers publish data and subscribers read and process this data. Data is transmitted in packets known as topics and each topic stores it’s own timestamp.&lt;/p&gt;

&lt;p&gt;The use cases for streaming in analytical data is low (which is the main data that data engineers mostly use). Streaming is more like owning a server, website or rest API rather than a batch pipeline or offline process. It is much more complex and some organizations even have different names for batch and streaming data engineers. (e.g., at Netflix, Data engineers handle batch processing whereas SWE, data handle stream data processing).&lt;/p&gt;

&lt;p&gt;In terms of technology, Apache spark is used for batch processing, Apache Kafka is used for streaming and Apache Flink supports both batch and stream processing but it was built for stream processing.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Exposure: Visualization and Predictions
&lt;/h2&gt;

&lt;p&gt;In data engineering - especially in the context of modern data tooling like dbt - an exposure refers to the end-use or downstream dependency of data models that shows where and how the data is being used outside of the transformation layer. Example exposures can be assets such as dashboards, machine learning models, external reports and APIs.&lt;/p&gt;

&lt;p&gt;My exposure was, of course, the Looker Studio dashboard (shown below).&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%2Fmorshsfaxmv6poun7w9z.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%2Fmorshsfaxmv6poun7w9z.png" alt="Image description" width="800" height="663"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I only used my final fact table in the dashboard as I had condensed all my previous staging and dimension tables into it using dbt.&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%2Fhab9egajsuntltt20g11.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%2Fhab9egajsuntltt20g11.png" alt="dbt data modelling" width="800" height="635"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Some of the insights I got were that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Software development&lt;/strong&gt; is the highest paying industry.&lt;/li&gt;
&lt;li&gt;Top-paying skills: &lt;strong&gt;Sales, IT, Management, Manufacturing&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Average required experience: &lt;strong&gt;6 years&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I learned a lot during the Zoomcamp and capstone project. The hands-on nature, real-world tooling, and community support made the journey insightful and practical.&lt;/p&gt;

&lt;p&gt;If you're interested in data engineering, I highly recommend joining a &lt;strong&gt;live cohort&lt;/strong&gt; of the DataTalks Club Zoomcamp to get the full experience and earn certification.&lt;/p&gt;

&lt;p&gt;This article was just a &lt;strong&gt;high-level tour of the data engineering landscape&lt;/strong&gt;—feel free to dig deeper into any concept that intrigued you.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Bon voyage!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>terraform</category>
      <category>gcp</category>
      <category>airflow</category>
    </item>
    <item>
      <title>Terraform on AWS: An introductory guide</title>
      <dc:creator>Benson King'ori</dc:creator>
      <pubDate>Wed, 05 Feb 2025 09:04:01 +0000</pubDate>
      <link>https://dev.to/aws-builders/terraform-on-aws-an-introductory-guide-5dfb</link>
      <guid>https://dev.to/aws-builders/terraform-on-aws-an-introductory-guide-5dfb</guid>
      <description>&lt;h3&gt;
  
  
  &lt;strong&gt;Terraform Overview&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;When people hear the term &lt;strong&gt;terraform&lt;/strong&gt;, they often think of &lt;strong&gt;terraforming planets&lt;/strong&gt;—a concept popularized by scientists and visionaries like &lt;strong&gt;Elon Musk&lt;/strong&gt;, who envisions making &lt;strong&gt;Mars habitable&lt;/strong&gt;. In this case, terraform means developing a rock or dead planet that is inhabitable so that it can have the necessary conditions to be able to sustain life. Terraforming Mars would involve generating an &lt;strong&gt;atmosphere&lt;/strong&gt;, introducing &lt;strong&gt;water sources&lt;/strong&gt;, and fostering &lt;strong&gt;plant life&lt;/strong&gt; to create conditions where humans could survive. Similarly, in the world of &lt;strong&gt;software development&lt;/strong&gt;, HashiCorp’s &lt;strong&gt;Terraform&lt;/strong&gt; follows the same principle—except instead of reshaping planets, it transforms &lt;strong&gt;cloud platforms&lt;/strong&gt; like &lt;strong&gt;AWS, GCP, and vSphere&lt;/strong&gt;, or on premise resources, into structured environments where applications can thrive. Just as planetary terraforming establishes the foundation for life, &lt;strong&gt;Terraform as Infrastructure-as-Code (IaC)&lt;/strong&gt; lays the groundwork for scalable and automated infrastructure where software can run seamlessly.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;What is Terraform?&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Terraform is an &lt;strong&gt;Infrastructure as Code (IaC)&lt;/strong&gt; tool developed by HashiCorp. It allows users to define cloud and on-premises infrastructure using human-readable configuration files. The tool provides a &lt;strong&gt;consistent workflow&lt;/strong&gt; to provision, manage, and automate infrastructure across its lifecycle.  &lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Why Use Terraform?&lt;/strong&gt;
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Simplicity&lt;/strong&gt; – All infrastructure is defined in a single file, making it easy to track changes.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collaboration&lt;/strong&gt; – Code can be stored in version control systems like GitHub for team collaboration.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reproducibility&lt;/strong&gt; – Configurations can be reused for different environments (e.g., development and production).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource Cleanup&lt;/strong&gt; – Ensures unused resources are properly destroyed to avoid unnecessary costs.
&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;What Terraform is NOT&lt;/strong&gt;
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Not a Software Deployment Tool&lt;/strong&gt; – It doesn’t manage or update software on existing infrastructure.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cannot Modify Immutable Resources&lt;/strong&gt; – Some changes (e.g., VM type) require destroying and recreating the resource.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does Not Manage External Resources&lt;/strong&gt; – Terraform only manages what is explicitly defined in its configuration files.
&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Terraform Workflow&lt;/strong&gt;
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Terraform Installed Locally&lt;/strong&gt; – The CLI runs on a user’s machine.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uses Providers&lt;/strong&gt; – These connect Terraform to cloud services (AWS, Azure, GCP, etc.).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication Required&lt;/strong&gt; – API keys or service accounts authenticate access to cloud platforms.
&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Terraform Installation&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Go to this &lt;a href="https://developer.hashicorp.com/terraform/install" rel="noopener noreferrer"&gt;link&lt;/a&gt; and follow the command that match your system’s specification&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Key Terraform Commands&lt;/strong&gt;
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;terraform init&lt;/code&gt;&lt;/strong&gt; – Downloads provider plugins and initializes the working directory.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;terraform plan&lt;/code&gt;&lt;/strong&gt; – Shows what changes Terraform will make before applying them.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;terraform apply&lt;/code&gt;&lt;/strong&gt; – Provisions the defined infrastructure.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;terraform destroy&lt;/code&gt;&lt;/strong&gt; – Removes all resources defined in the Terraform configuration.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Terraform Files and Their Generation&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Terraform uses several files to manage your infrastructure state, configuration, and dependencies. Below is an overview of the key files, what they represent, and which Terraform command triggers their creation or update:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Configuration Files (&lt;code&gt;*.tf&lt;/code&gt; files):&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Purpose:&lt;/strong&gt;
These files (such as &lt;code&gt;main.tf&lt;/code&gt;, &lt;code&gt;variables.tf&lt;/code&gt;, &lt;code&gt;outputs.tf&lt;/code&gt;, etc.) are written by you to define your infrastructure. They describe the resources you wish to provision and how they interrelate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When They Are Created:&lt;/strong&gt;
You create these manually. They form the blueprint for Terraform to understand and manage your environment.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Terraform State File (&lt;code&gt;terraform.tfstate&lt;/code&gt;):&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Purpose:&lt;/strong&gt;
This file tracks the current state of your infrastructure. It maps your configuration to the real-world resources, ensuring that Terraform can determine what changes need to be made.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When It Is Generated/Updated:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;After &lt;code&gt;terraform apply&lt;/code&gt;:&lt;/strong&gt;
When you run &lt;code&gt;terraform apply&lt;/code&gt;, Terraform provisions your infrastructure based on your configuration. During this process, it creates or updates the &lt;code&gt;terraform.tfstate&lt;/code&gt; file with the current state of the resources.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;After &lt;code&gt;terraform destroy&lt;/code&gt;:&lt;/strong&gt;
Similarly, when you destroy resources, the state file is updated to reflect that the resources no longer exist.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Terraform Lock File (&lt;code&gt;.terraform.lock.hcl&lt;/code&gt;):&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Purpose:&lt;/strong&gt;
This file locks the versions of the provider plugins used in your configuration to ensure consistency and prevent unexpected changes from newer versions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When It Is Generated/Updated:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;After &lt;code&gt;terraform init&lt;/code&gt;:&lt;/strong&gt;
Running &lt;code&gt;terraform init&lt;/code&gt; downloads the required provider plugins and creates the &lt;code&gt;.terraform.lock.hcl&lt;/code&gt; file. This ensures that every team member or CI/CD pipeline uses the same provider versions.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Terraform Directory (&lt;code&gt;.terraform&lt;/code&gt;):&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Purpose:&lt;/strong&gt;
This hidden directory stores downloaded provider plugins, module sources, and backend configuration. It is essential for Terraform's operation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When It Is Generated:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;After &lt;code&gt;terraform init&lt;/code&gt;:&lt;/strong&gt;
The &lt;code&gt;.terraform&lt;/code&gt; directory is automatically created when you initialize your Terraform working directory using &lt;code&gt;terraform init&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Plan Output File (optional):&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Purpose:&lt;/strong&gt;
If you choose to save the execution plan to a file (using the &lt;code&gt;-out&lt;/code&gt; flag with &lt;code&gt;terraform plan&lt;/code&gt;), this binary file captures the set of changes Terraform intends to make.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When It Is Generated:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;After &lt;code&gt;terraform plan -out=&amp;lt;filename&amp;gt;&lt;/code&gt;:&lt;/strong&gt;
Running this command generates a plan file that can later be applied using &lt;code&gt;terraform apply &amp;lt;filename&amp;gt;&lt;/code&gt;. This is useful for reviewing changes or automating deployment workflows.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Configure aws credentials locally&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;This is important if you want to be able to access your aws account and resources on your terminal or inside your code using an sdk such as boto3.&lt;br&gt;
There might be other ways to do this but I will list 2 here:&lt;br&gt;
aws configure&lt;br&gt;
Export the credentials as environment variables&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;strong&gt;1. aws configure&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;To use this, you will need to install aws-cli which you can do from &lt;a href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I found that  &lt;strong&gt;&lt;code&gt;sudo apt install aws-cli&lt;/code&gt;&lt;/strong&gt; or  &lt;strong&gt;&lt;code&gt;pip3 install aws-cli&lt;/code&gt;&lt;/strong&gt; worked just as well for me.&lt;/p&gt;

&lt;p&gt;You can confirm that you have installed it by checking it’s version as below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws --version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To configure your credentials locally, you will need to create an IAM user and give them some permissions.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log onto your aws console&lt;/li&gt;
&lt;li&gt;Navigate to the IAM section&lt;/li&gt;
&lt;li&gt;Create a new user and grant them the required permissions&lt;/li&gt;
&lt;li&gt;Download the access key and secret key as csv of that user and store it securely. I prefer this rather than just copying them from the console. Just make sure not to commit them publicly. For example, if you’re working on a repository that has a remote version in github/gitlab/bitbucket etc then consider adding the csv to your git ignore before adding, committing and pushing changes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now, run the following command to configure your credentials locally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws configure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will be prompted to enter the access key id, secret access key, region, and output format (choose json, text, or table, default is json). Once you fill them, it creates 2 files in the ~/.aws directory: credentials and config. The credentials file contains the access key and secret key. The config file contains the region and output format.&lt;/p&gt;

&lt;p&gt;To test if you have access to your aws account from your local terminal, create a dummy s3 bucket then run the following command to list your buckets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws s3 ls
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;strong&gt;2. Export the credentials as environment variables&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Run the following command on your terminal, replacing the stringed text with your actual values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export AWS_ACCESS_KEY_ID="your-access-key-id"
export AWS_SECRET_ACCESS_KEY="your-secret-access-key"
export AWS_DEFAULT_REGION="your-region"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform, Boto3, and AWS CLI will automatically use these from the environment variables or the files in the ~/.aws directory&lt;/p&gt;

&lt;h3&gt;
  
  
  *&lt;em&gt;Managing resources on terraform *&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;Create a main.tf file in the folder you are working in and save the following code in the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
      version = "5.85.0"
    }
  }
}

provider "aws" {
  # Configuration options
  region = "your-region"
}

# Variable definitions
variable "aws_region" {
  description = "AWS region for resources"
  type        = string
  default     = "your-region"
}

resource "aws_s3_bucket" "example" {
  bucket = "my-tf-test-bucket-${random_id.bucket_suffix.hex}" # Make bucket name unique

  tags = {
    Name        = "My bucket"
    Environment = "Dev"
  }
}

# Add a random suffix to ensure bucket name uniqueness
resource "random_id" "bucket_suffix" {
  byte_length = 4
}

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

&lt;/div&gt;



&lt;p&gt;Replace “your-region” with your actual region.&lt;/p&gt;

&lt;p&gt;Now we can run the terraform commands.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;1. Initialize terraform in your folder&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Run 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;terraform init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It initializes the backend and provider plugins. It also creates a lock file .terraform.lock.hcl to record the provider selections. It also creates a .terraform folder. Include this file in your version control repository so that Terraform can guarantee to make the same selections by default when&lt;br&gt;
you run "terraform init" in the future. &lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;strong&gt;2. Plan&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Run the following command to see any changes that are required for your infrastructure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform plan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It generates an execution plan based on the code in main.tf. At the end of the output there is a summary that looks like the below:&lt;/p&gt;

&lt;p&gt;Plan: 2 to add, 0 to change, 0 to destroy.&lt;/p&gt;

&lt;p&gt;Changes will be suggested which you can agree to by running the apply command explained below. You can save the plan by using the out flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform plan -out=filepath-to-save-file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;strong&gt;3. Apply&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;To apply the changes suggested run the command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will be prompted to confirm by typing yes. Be careful as this does create the resources thus you will incur costs on your aws account unless you have cloud credits or are using the free tier.&lt;/p&gt;

&lt;p&gt;After typing yes, go to the console and navigate to the s3 section. Check if you have a new bucket created. Alternatively, you can run the following command to list your buckets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws s3 ls
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you had previously saved your plan to a file, please run the following command to apply that plan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform apply “filename”
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;strong&gt;4. Delete&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Run the following command to delete the resources provisioned by terraform&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform destroy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your resources marked for deletion will be listed and you will again be prompted for confirmation. Confirm by typing yes.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Summary&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;This guide walks you through setting up AWS credentials locally using both the AWS CLI configuration and environment variables, and demonstrates how to manage AWS resources with Terraform. You learned how to write a basic Terraform configuration to create an S3 bucket, initialize your project, preview changes with a plan, apply those changes, and ultimately destroy the resources when they are no longer needed. This systematic approach to infrastructure management not only ensures consistency and repeatability but also aligns with modern DevOps best practices.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>terraform</category>
      <category>infrastructureascode</category>
      <category>devops</category>
    </item>
    <item>
      <title>Deploy Your Site in Seconds Using AWS Amplify</title>
      <dc:creator>Benson King'ori</dc:creator>
      <pubDate>Sun, 12 Jan 2025 05:27:43 +0000</pubDate>
      <link>https://dev.to/aws-builders/deploy-your-site-in-seconds-using-aws-amplify-3m50</link>
      <guid>https://dev.to/aws-builders/deploy-your-site-in-seconds-using-aws-amplify-3m50</guid>
      <description>&lt;p&gt;In today’s fast-paced digital world, deploying a web application quickly and reliably is crucial. AWS Amplify provides a seamless way to get your site online in seconds. To demonstrate its power, I built &lt;strong&gt;Choicepool&lt;/strong&gt;, an interactive web app designed to simplify two-way door decision-making through fun games like coin flips, dice rolls, and rock-paper-scissors. The project combines interactivity with ease of deployment, showcasing how AWS Amplify can power robust web applications.  &lt;/p&gt;

&lt;p&gt;This post will introduce the Choicepool project, compare various hosting options, highlight the benefits of AWS Amplify, and guide you through deploying your site using Amplify and its integrations.  &lt;/p&gt;




&lt;h2&gt;
  
  
  About Choicepool
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Choicepool&lt;/strong&gt; is a simple yet engaging web app built with HTML, CSS, and JavaScript. It allows users to input their choices and randomly pick one through gamified experiences. By leveraging games, Choicepool makes decision-making both efficient and fun. The app was deployed using AWS Amplify, illustrating how simple and fast deployment can be when using the right tools.&lt;/p&gt;

&lt;p&gt;You can find choicepool &lt;a href="https://main.d1h51wsna4r80m.amplifyapp.com/index.html" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Hosting Options for Web Apps
&lt;/h3&gt;

&lt;p&gt;When deploying a web app, choosing the right hosting provider is vital. Below are common hosting options, along with their pros and cons:  &lt;/p&gt;

&lt;h3&gt;
  
  
  1. &lt;strong&gt;GitHub Pages&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pros&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Free for public repositories.
&lt;/li&gt;
&lt;li&gt;Easy to deploy static websites.
&lt;/li&gt;
&lt;li&gt;Well-integrated with GitHub repositories.
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Cons&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Limited to static sites; no backend support.
&lt;/li&gt;
&lt;li&gt;Lacks advanced scalability options.
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. &lt;strong&gt;Netlify&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pros&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Simple CI/CD for static and serverless apps.
&lt;/li&gt;
&lt;li&gt;Built-in features like form handling and serverless functions.
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Cons&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Costs can increase with higher traffic.
&lt;/li&gt;
&lt;li&gt;Less powerful integration with backend services.
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. &lt;strong&gt;Vercel&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pros&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Optimized for React and Next.js apps.
&lt;/li&gt;
&lt;li&gt;Automatic builds and global CDN.
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Cons&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Limited support for backend integrations.
&lt;/li&gt;
&lt;li&gt;Pricing tiers can become costly for large teams.
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. &lt;strong&gt;AWS Amplify&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;AWS Amplify is a game-changer for hosting web apps, offering seamless deployment and integration with other AWS services.  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ease and Speed of Deployment&lt;/strong&gt;: Amplify makes deployment fast, whether through uploading zipped files or connecting a Git repository.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Powerful Integrations&lt;/strong&gt;: Supports services like Amazon S3 for file storage, DynamoDB for databases, and Lambda for serverless functions.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt;: Automatically scales with traffic demands.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built-in CI/CD Pipelines&lt;/strong&gt;: Automates builds and deployments directly from your Git repositories.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rich Feature Set&lt;/strong&gt;: Includes hosting, authentication, analytics, and AI/ML capabilities via other AWS services.
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Initial learning curve for those new to AWS services.
&lt;/li&gt;
&lt;li&gt;Costs can increase with extensive feature usage.
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step-by-Step Guide to Hosting with AWS Amplify
&lt;/h2&gt;

&lt;p&gt;Amplify simplifies the deployment process, making it accessible to both beginners and experienced developers. Here’s how you can deploy your site:  &lt;/p&gt;

&lt;h3&gt;
  
  
  1. &lt;strong&gt;Prepare Your Web App&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Ensure your web app is ready for deployment. This involves:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Testing your app locally.
&lt;/li&gt;
&lt;li&gt;Ensuring all assets (CSS, JavaScript, images) are included.
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. &lt;strong&gt;Create an AWS Account&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;If you don’t already have an AWS account, create one at &lt;a href="https://aws.amazon.com" rel="noopener noreferrer"&gt;AWS&lt;/a&gt;. Navigate to the AWS Management Console and search for Amplify.  &lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;strong&gt;Deploy Your Site&lt;/strong&gt;
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Option 1: Upload a Zipped File
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Compress your project folder into a &lt;code&gt;.zip&lt;/code&gt; file.
&lt;/li&gt;
&lt;li&gt;Go to the Amplify console and select &lt;strong&gt;Host a Web App&lt;/strong&gt;.
&lt;/li&gt;
&lt;li&gt;Upload your zipped file.
&lt;/li&gt;
&lt;li&gt;Amplify will handle the rest, generating a live URL for your site.
&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Option 2: Deploy from GitHub, GitLab, or Bitbucket
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;In the Amplify console, select &lt;strong&gt;Host a Web App&lt;/strong&gt;.
&lt;/li&gt;
&lt;li&gt;Connect your GitHub, GitLab, or Bitbucket account.
&lt;/li&gt;
&lt;li&gt;Choose the repository and branch you want to deploy.
&lt;/li&gt;
&lt;li&gt;Amplify will build and deploy your app.
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  4. &lt;strong&gt;View Your Hosted Site&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Once deployed, Amplify generates a live URL for your site. You can share this link or configure a custom domain.  &lt;/p&gt;




&lt;h2&gt;
  
  
  Enhancing Deployments with Amazon Q Developer
&lt;/h2&gt;

&lt;p&gt;Amazon Q Developer, a powerful tool for AWS users, enhances Amplify’s deployment capabilities by integrating intelligence into the CI/CD pipeline.  &lt;/p&gt;

&lt;h3&gt;
  
  
  Advantages of Amazon Q Developer
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automated Insights&lt;/strong&gt;: Identify potential issues during deployment.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimized Resource Allocation&lt;/strong&gt;: Suggests configurations to reduce costs.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simplified Integration&lt;/strong&gt;: Easily integrates with other AWS services.
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How to Gain Access
&lt;/h3&gt;

&lt;p&gt;Amazon Q Developer is available through the AWS Management Console. To use it:  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to the AWS Marketplace.
&lt;/li&gt;
&lt;li&gt;Search for Amazon Q Developer.
&lt;/li&gt;
&lt;li&gt;Follow the instructions to enable it for your account.
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Why AWS Amplify Stands Out
&lt;/h2&gt;

&lt;p&gt;Amplify-hosted sites are highly versatile and capable. As demonstrated by &lt;strong&gt;Choicepool&lt;/strong&gt;, a simple app built entirely with HTML, CSS, and JavaScript, Amplify can host interactive applications with minimal setup.  &lt;/p&gt;

&lt;h3&gt;
  
  
  Examples of Amplify’s Power:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;API Calls to ML Models&lt;/strong&gt;: With JavaScript, you can make API calls to AWS SageMaker endpoints, enabling advanced AI capabilities in your app.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database Integrations&lt;/strong&gt;: Amplify supports direct integration with AWS DynamoDB, allowing real-time data storage and retrieval.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standalone Apps&lt;/strong&gt;: Amplify handles hosting and scaling, so even complex apps can run independently without additional backend infrastructure.
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What’s Next for Choicepool
&lt;/h2&gt;

&lt;p&gt;AWS Amplify continues to push the boundaries of what’s possible with web app hosting. For Choicepool, future updates might include:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sound effects and animations for an even more interactive experience.
&lt;/li&gt;
&lt;li&gt;Support for multiple languages to reach a broader audience.
&lt;/li&gt;
&lt;li&gt;User preference tracking through Amplify’s built-in analytics and backend integrations.
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;AWS Amplify is a powerful tool for developers seeking fast, scalable, and feature-rich hosting. By enabling rapid deployment and seamless integration with AWS services, Amplify allows developers to focus on building impactful applications. Whether it’s a simple decision-making app like &lt;strong&gt;Choicepool&lt;/strong&gt; or a complex AI-driven platform, Amplify ensures your project is ready to reach the world in seconds.  &lt;/p&gt;

&lt;p&gt;Ready to deploy your site? Head to &lt;a href="https://aws.amazon.com/amplify/" rel="noopener noreferrer"&gt;AWS Amplify&lt;/a&gt; and get started today!  &lt;/p&gt;




</description>
    </item>
    <item>
      <title>Deep Fake, Easily Made</title>
      <dc:creator>Benson King'ori</dc:creator>
      <pubDate>Thu, 22 Feb 2024 14:33:53 +0000</pubDate>
      <link>https://dev.to/aws-builders/deep-fake-easily-made-279i</link>
      <guid>https://dev.to/aws-builders/deep-fake-easily-made-279i</guid>
      <description>&lt;p&gt;Refacer is an open source library that allows easy replacement of faces in a video. In this article, I will detail how to use it in order to do exactly that. &lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;A laptop/desktop device (I suppose even a tablet could work)&lt;/li&gt;
&lt;li&gt;A Github Account&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You will also need to prepare the files you will need in the refacing process -&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Source Video: the original video that you wish to clone; for this tutorial, we’ll use a scene from the “The Harder They Fall.” For the initial test run, I would suggest you use a short video that is a minute-long or even shorter.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Target Face Images: Images of the faces you intend to manipulate. These could be screenshots from the video or other sources. For this tutorial, my target face was that of Cherokee Bill played by LaKeith Stanfield.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Replacement Faces Images: Images of the faces that will replace the original faces in the target video. Please make sure you have the consent to use these pictures / faces from the person. For this tutorial I used my own face.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Refacing
&lt;/h2&gt;

&lt;p&gt;To create the deep fake, we will use the refacer repository available on Github &lt;a href="https://github.com/xaviviro/refacer?ref=alxappliedai.com" rel="noopener noreferrer"&gt;here&lt;/a&gt;. Before using the app make sure you have read the disclaimer at the end of this project or on the GitHub repository. You can access the Refacer using a &lt;a href="https://colab.research.google.com/drive/1gyhEp2WDvFhPJ5YthN2W0XccU700TSJv?usp=sharing&amp;amp;ref=alxappliedai.com" rel="noopener noreferrer"&gt;Google colab notebook&lt;/a&gt; but since that did not work for me, this tutorial will guide you through the longer route - running the project locally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup
&lt;/h3&gt;

&lt;p&gt;I will assume that you are working on linux or any other unix-based system (as is the Godly thing to do as a developer).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to the directory you want to store the project in (I recommend creating a new folder altogether)&lt;/li&gt;
&lt;li&gt;Download this file &lt;a href="https://github.com/facefusion/facefusion-assets/releases/download/models/inswapper_128.onnx?ref=alxappliedai.com" rel="noopener noreferrer"&gt;inswapper_128.onnx&lt;/a&gt; and place it inside the folder you just created.&lt;/li&gt;
&lt;li&gt;In the terminal, navigate to the folder you just created&lt;/li&gt;
&lt;li&gt;Clone the Rafacer repository from GitHub using the below command: &lt;code&gt;git clone https://github.com/xaviviro/refacer.git&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Navigate to the refacer directory by using the command &lt;code&gt;cd refacer&lt;/code&gt; if you are on linux/mac &lt;del&gt;or &lt;code&gt;chdir refacer&lt;/code&gt; if you are on windows&lt;/del&gt;
&lt;/li&gt;
&lt;li&gt;Open the requirements.txt file and replace gradio==3.33.1 with gradio==3.36.1 and save &amp;amp; close&lt;/li&gt;
&lt;li&gt;Install packages: &lt;code&gt;pip install -r requirements.txt&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Run the app: &lt;code&gt;python app.py&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Finally, open your web browser and navigate to the following address: &lt;code&gt;http://127.0.0.1:7680&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Refacer Interface
&lt;/h3&gt;

&lt;p&gt;On the specified port, you should see an interface that looks like the one below:&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%2Fn8kay6p6jahkeiaswkjq.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%2Fn8kay6p6jahkeiaswkjq.png" alt="Reface User Interface on the Web" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The main sections are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Original Video Upload: Upload the source video to this section.&lt;/li&gt;
&lt;li&gt;Target Faces Placement: Place the faces in the video that you want to replace; up to five faces are supported.&lt;/li&gt;
&lt;li&gt;Replacement Faces Placement: Position the faces that will replace the corresponding faces uploaded to section (2).&lt;/li&gt;
&lt;li&gt;Output File Display: The resulting file will be displayed here.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Upload the required files
&lt;/h3&gt;

&lt;p&gt;Upload the respective files to their designated sections and click “Reface” (The big orange button at the bottom of the page). This action initiates a process that will “reface” all frames in the video using the provided faces. Please note that this process may take some time.&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%2F884l6k6rimxzgbrxwkd0.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%2F884l6k6rimxzgbrxwkd0.png" alt="Refacer Interface with uploaded media" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can check the terminal to see progress being made. I would also recommend that if you have a light-weight device, you could close all other running apps and leave the device for refacing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Accessing your video
&lt;/h3&gt;

&lt;p&gt;Your output/refaced video will be in the /out folder. For the full path, please check your terminal. You can also view the refaced video on the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sharing
&lt;/h2&gt;

&lt;p&gt;Please be sure to share your refaced video like I did &lt;a href="https://www.linkedin.com/posts/benson-mugure-017153196_alxabrai-deepfake-appliedai-activity-7142568176705347584-PVdY?utm_source=share&amp;amp;utm_medium=member_desktop" rel="noopener noreferrer"&gt;here&lt;/a&gt; and if you can, tag me!&lt;/p&gt;

&lt;p&gt;Till next time comrades, may the force  be with you!&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Feature Selection On Zindi Starter Notebook</title>
      <dc:creator>Benson King'ori</dc:creator>
      <pubDate>Wed, 07 Feb 2024 13:32:02 +0000</pubDate>
      <link>https://dev.to/aws-builders/feature-selection-on-zindi-starternotebook-1f7l</link>
      <guid>https://dev.to/aws-builders/feature-selection-on-zindi-starternotebook-1f7l</guid>
      <description>&lt;p&gt;On the path to making a predictive model, we are sometimes faced with the choice to cherry pick amongst our list of features. (If the term cherrypick still gives you nightmares from your adventures in gitland then here’s a fitbump 👊). Perhaps this is because of the high dimensionality of our data or just in the cyclic model hyperparameter finetuning. Regardless of the reason, learning the different ways you can employ to decide which features to use and which to not use can end up improving model performance or even reducing computational time and complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  EDA, Cleaning and Preprocessing
&lt;/h2&gt;

&lt;p&gt;For this exercise, I employed the &lt;a href="https://zindi.africa/competitions/financial-inclusion-in-africa" rel="noopener noreferrer"&gt;Financial Inclusion in Africa Competition in Zindi&lt;/a&gt; because of 2 reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Readily available data&lt;/li&gt;
&lt;li&gt;A starter notebook that deals with EDA and basic data cleaning&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As such, the very next step from these offerings in the competition is to do feature engineering. If you want to join in for a follow-along kind of reading, feel free to download the data and starter Notebook from Zindi &lt;a href="https://zindi.africa/competitions/financial-inclusion-in-africa/data" rel="noopener noreferrer"&gt;here&lt;/a&gt; (You might need to create a Zindi account though).&lt;/p&gt;

&lt;p&gt;The starter notebook makes use of the following beautiful function that is used to transform both the test and the train datasets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# function to preprocess our data from train models
def preprocessing_data(data):

    # Convert the following numerical labels from interger to float
    float_array = data[["household_size", "age_of_respondent", "year"]].values.astype(float)

    # categorical features to be onverted to One Hot Encoding
    categ = ["relationship_with_head",
             "marital_status",
             "education_level",
             "job_type",
             "country"]

    # One Hot Encoding conversion
    data = pd.get_dummies(data, prefix_sep="_", columns=categ)

    # Label Encoder conversion
    data["location_type"] = le.fit_transform(data["location_type"])
    data["cellphone_access"] = le.fit_transform(data["cellphone_access"])
    data["gender_of_respondent"] = le.fit_transform(data["gender_of_respondent"])

    # drop uniquid column
    data = data.drop(["uniqueid"], axis=1)

    # scale our data into range of 0 and 1
    scaler = MinMaxScaler(feature_range=(0, 1))
    data = scaler.fit_transform(data)

    return data                  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unfortunately, after the transformations, we have a long list of 37 features. Although we can use all of them it is advisable to select the best features which would help us best predict the target variable.&lt;/p&gt;

&lt;p&gt;After the processing, we are left with two numpy array sets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# preprocess the train data 
processed_train = preprocessing_data(X_train)
processed_test = preprocessing_data(test)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I converted the arrays to dataframes and then saved them to CSV files for easy processing in another notebook.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Save to csv
processed_train = pd.DataFrame(processed_train)
processed_test = pd.DataFrame(processed_test)

processed_train.to_csv('data/preprocessed_train.csv', index = False)
processed_test.to_csv('data/preprocessed_test.csv', index = False)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I could have simply used them in the starter Notebook but I wanted to have a saved version of my pre-processed data for future experimentation with feature Engineering and other techniques that would better model performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feature Selection
&lt;/h2&gt;

&lt;p&gt;In another &lt;a href="https://gist.github.com/Virgo-Alpha/eb2daeccf24194d1b38c9c4c2d00015b" rel="noopener noreferrer"&gt;notebook&lt;/a&gt;, I imported the required libraries as well as the datasets:&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%2Foc0ohmqdvl5zbg9eidnn.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%2Foc0ohmqdvl5zbg9eidnn.png" alt="Code block" width="545" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Afterwards, I started the different ways to select features.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Univariate Statistics
&lt;/h3&gt;

&lt;p&gt;Statistical tests can be used to select those features that have the strongest relationship with the output variable.&lt;/p&gt;

&lt;p&gt;The scikit-learn library provides the SelectKBest class that can be used with a suite of different statistical tests to select a specific number of features.&lt;/p&gt;

&lt;p&gt;Many different statistical test scan be used with this selection method. For example the ANOVA F-value method is appropriate for numerical inputs and categorical data. This can be used via the f_classif() function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from sklearn.feature_selection import SelectKBest
from numpy import set_printoptions
from sklearn.feature_selection import f_classif

# feature extraction
test = SelectKBest(score_func=f_classif, k=4)
fit = test.fit(X, y)
# summarize scores
set_printoptions(precision=3)
print(fit.scores_)
features = fit.transform(X)
# summarize selected features
print(features[0:5,:])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We will use the chi method to select the 10 best features using this method in the example below.&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%2F5fwnd3ulg0eu6f80tl0w.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%2F5fwnd3ulg0eu6f80tl0w.png" alt="Code Block" width="800" height="655"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Alternatives to ch-squared and ANOVA F-value (all imported from sklearn.feature_selection)
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Mutual Information&lt;/em&gt;: Measures the mutual dependence between two variables.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Information Gain&lt;/em&gt;: Measures the reduction in entropy achieved by splitting data on a particular feature.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Correlation Coefficient&lt;/em&gt;: Measures the linear relationship between two numerical variables.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Distance Correlation&lt;/em&gt;: Measures the dependence between two random variables.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;ReliefF&lt;/em&gt;: Computes feature importance based on the ability to distinguish between instances of different classes.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  2. Feature Importance
&lt;/h3&gt;

&lt;p&gt;Bagged decision trees like Random Forest and Extra Trees can be used to estimate the importance of features.&lt;/p&gt;

&lt;p&gt;Note: Your results may vary given the stochastic nature of the algorithm or evaluation procedure, or differences in numerical precision. Consider running the example a few times and compare the average outcome.&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%2Fq4dr2kmwynsmp52mwmux.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%2Fq4dr2kmwynsmp52mwmux.png" alt="Code Block" width="800" height="620"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Recursive Feature Elimination
&lt;/h3&gt;

&lt;p&gt;The Recursive Feature Elimination (or RFE) works by recursively removing attributes and building a model on those attributes that remain.&lt;/p&gt;

&lt;p&gt;It uses the model accuracy to identify which attributes (and combination of attributes) contribute the most to predicting the target attribute.&lt;/p&gt;

&lt;p&gt;You can learn more about the RFE class in the scikit-learn documentation.&lt;/p&gt;

&lt;p&gt;The example below uses RFE with the logistic regression algorithm to select the top 10 features. The choice of algorithm does not matter too much as long as it is skillful and consistent.&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%2Fxgyvnzx3kuxjiwpezwst.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%2Fxgyvnzx3kuxjiwpezwst.png" alt="Code Block" width="800" height="595"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Principal Component Analysis (PCA)
&lt;/h3&gt;

&lt;p&gt;Principal Component Analysis(or PCA) uses linear algebra to transform the dataset into a compressed form.&lt;/p&gt;

&lt;p&gt;Generally this is called a data reduction technique. A property of PCA is that you can choose the number of dimensions or principal component in the transformed result.&lt;/p&gt;

&lt;p&gt;In the example below, we use PCA and select 3 principal components.&lt;/p&gt;

&lt;p&gt;Learn more about the PCA class in scikit-learn by reviewing the PCA API. Dive deeper into the math behind PCA on the Principal Component Analysis Wikipedia article.&lt;/p&gt;

&lt;h4&gt;
  
  
  PCA Usefulness:
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Dimension reduction&lt;/em&gt;&lt;/strong&gt;:  When dealing with datasets containing a large number of features, PCA can help reduce the dimensionality while preserving most of the variability in the data. This can lead to simpler models, reduced computational complexity, and alleviation of the curse of dimensionality. An example is here where we have 37 features&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Data exploration/visualization&lt;/em&gt;&lt;/strong&gt;: PCA can be used to visualize high-dimensional data in lower-dimensional space (e.g., 2D or 3D) for exploratory data analysis and visualization. This can help uncover patterns, clusters, and relationships between variables.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Noise reduction&lt;/em&gt;&lt;/strong&gt;: PCA identifies and removes redundant information (noise) in the data by focusing on the directions of maximum variance. This can lead to improved model performance by reducing overfitting and improving generalization&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Feature Creation&lt;/em&gt;&lt;/strong&gt;: PCA can be used to create new composite features (principal components) that capture the most important information in the original features. These components may be more informative or less correlated than the original features, potentially enhancing the performance of machine learning algorithms.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Reducing computational Complexity&lt;/em&gt;&lt;/strong&gt;: In cases where the original dataset is large and computationally expensive to process, PCA can be used to reduce the size of the dataset without sacrificing much information. This can lead to faster training and inference times for machine learning models.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Addressing multicollinearity in the features&lt;/em&gt;&lt;/strong&gt;: PCA can mitigate multicollinearity issues by transforming correlated features into orthogonal principal components. This can improve the stability and interpretability of regression models.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&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%2F6qtgkmpq7xa3kzy5s9uj.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%2F6qtgkmpq7xa3kzy5s9uj.png" alt="Code Block" width="800" height="632"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Correlation Matrix With HeatMap
&lt;/h3&gt;

&lt;p&gt;A correlation matrix is a square matrix that shows the correlation coefficients between pairs of variables in a dataset. Each cell in the matrix represents the correlation coefficient between two variables.&lt;br&gt;
The correlation coefficient ranges from -1 to 1, where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1 indicates a perfect positive correlation,&lt;/li&gt;
&lt;li&gt;0 indicates no correlation, and&lt;/li&gt;
&lt;li&gt;-1 indicates a perfect negative correlation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A heatmap is a graphical representation of the correlation matrix, where each cell's color indicates the strength and direction of the correlation between two variables.&lt;br&gt;
Darker colors (e.g., red) represent stronger positive correlations, while lighter colors (e.g., blue) represent stronger negative correlations.&lt;br&gt;
The diagonal of the heatmap typically shows correlation values of 1, as each variable is perfectly correlated with itself.&lt;br&gt;
By visualizing the correlation matrix as a heatmap, you can quickly identify patterns of correlation between features.&lt;/p&gt;

&lt;p&gt;In the heatmap, you can look for clusters of high correlation (e.g., dark squares) to identify groups of features that are highly correlated with each other.&lt;br&gt;
Once identified, you can decide whether to keep, remove, or transform these features based on their importance to the model and their contribution to multicollinearity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#get correlations of each features in dataset
corrmat = X.corr()
top_corr_features = corrmat.index
plt.figure(figsize=(37,37))
#plot heat map
g=sns.heatmap(X[top_corr_features].corr(),annot=True,cmap="coolwarm")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In Conclusion, feature selection is an important part of the machine learning process which can have a myriad of benefits. There are a multitude of ways to go about it but depending on your particular use case, be it supervised or unsupervised, some may be better than others. For instance, in supervised learning as was our case above, we compared the features against their usefulness in determining a target variable. However, in unsupervised learning, there is no target variable. As such, most feature selection algorithms that are useful here compare the variables to each other, helping cull out the ones with high multicollinearity.&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%2Fh6eo2puh8u63j178bbpt.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%2Fh6eo2puh8u63j178bbpt.png" alt="Code Block" width="681" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Another important matrix in determining the Feature selection algorithm to use is the data types of your features. In general, different data types need different selection algorithms. Please see below:&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%2Ftr59f50uwc6c3vxg1je0.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%2Ftr59f50uwc6c3vxg1je0.png" alt="Code Block" width="762" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Have a happy time exploring the other algorithms and may the force be with you!&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>datascience</category>
      <category>python</category>
    </item>
  </channel>
</rss>
