<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Stephen Gilmore&apos;s blog</title><description>Stephen Gilmore&apos;s blog</description><link>https://sglmr.com/</link><language>en-us</language><item><title>I automated my kid&apos;s lunch menu</title><link>https://sglmr.com/blog/lunchbot/</link><guid isPermaLink="true">https://sglmr.com/blog/lunchbot/</guid><pubDate>Wed, 09 Sep 2026 12:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;./2026-09-09-lunchbot-ntfy-screenshot.png&quot; alt=&quot;Notification Screenshot&quot; /&gt;

My kid likes to eat hot lunch at school and is a very picky eater. Every night, we need to go look up tomorrow&apos;s lunch to figure out if she&apos;ll want to eat it. I bet everybody usually sticks the menu on their fridge. But since I&apos;m a nerd, I set up a daily notification with tomorrow&apos;s lunch.

This post walks through how I did it.

## Step 1: Get ntfy

&lt;a href=&quot;https://ntfy.sh&quot;&gt;ntfy&lt;/a&gt; is a useful and free push notification service. It lets you subscribe to topic(s) and it will notify you anytime something gets posted on your topic(s).

After downloading the app, pick a topic name. You might choose a topic like &lt;code&gt;{school}_lunch_menu&lt;/code&gt; where &lt;code&gt;{school}&lt;/code&gt; is the name of your school.

## Step 2: Sign up for Cloudflare

You&apos;ll have to sign up for a &lt;a href=&quot;https://www.cloudflare.com&quot;&gt;Cloudflare&lt;/a&gt; account. We’ll use Cloudflare’s “workers&quot; to run small scripts with a generous free tier.

## Step 3: Install Node.js and NPM

To build a Cloudflare Worker, you&apos;ll need Node.js on your computer. It comes bundled with npm (Node Package Manager).

Head over to &lt;a href=&quot;https://nodejs.org/en/download&quot;&gt;nodejs.org&lt;/a&gt; and download the LTS (Long Term Support) installer for your computer.

Run the installer and click through with the default settings.

To verify it worked, open your terminal and run node -v and npm -v.

## Step 4: Get the data

Our district&apos;s menu comes out as a calendar PDF. It would be annoying to try to retype the whole menu by hand. I automate it by uploading the menu with instructions to an AI chat bot.

These are the instructions:

&lt;code&gt;markdown **Role:** You are a data extraction assistant specializing in converting unstructured calendars into clean JSON.  **Task:** Please analyze the attached school lunch calendar PDF and extract the lunch menu into a structured JSON format.  **Output Requirements:**  1. **Format:** A single JSON object where the **key** is the date in `YYYY-MM-DD` format and the **value** is the main entree (e.g., &quot;Chicken Nuggets&quot; or &quot;Bean &amp;amp; Cheese Burrito&quot;). 2. **Date Context:** Use the Month and Year explicitly stated on the calendar (e.g., if the calendar says &quot;May&quot; and the year is 2026, the first day should be &quot;2026-05-01&quot;). 3. **Clean Data:** - Remove any extra text like &quot;Choice of milk,&quot; &quot;Fruit bar,&quot; or &quot;Side of corn.&quot; Only include the main entree.  - If a day has no meal listed (weekends or holidays), omit that key from the JSON entirely. - If the text spans multiple lines in one cell, combine it into a single line (e.g., &quot;Hand rolled\nBean Burrito&quot; becomes &quot;Hand rolled Bean Burrito&quot;). - The text should be in title case  4. **No Prose:** Output **only** the raw JSON block. Do not include introductory text or explanations.  **Example Target Structure:** { &quot;2026-05-01&quot;: &quot;Hand rolled Bean &amp;amp; Cheese Burrito&quot;, &quot;2026-05-04&quot;: &quot;Chicken Nuggets&quot;, &quot;2026-05-05&quot;: &quot;Turkey Tacos&quot; } &lt;/code&gt;

## Step 5: Write the script

This script has lived two different lives and a few enhancements. It lived for a year as a human-written Python script running on a server. After expelling server maintenance from my list of chores, I asked AI to convert it to Javascript that I could run on &lt;a href=&quot;https://www.cloudflare.com/products/workers/&quot;&gt;Cloudflare Workers&lt;/a&gt;.

&lt;strong&gt;How it works:&lt;/strong&gt;

1. The script runs every day.
2. If the day is Sunday-Thursday, it creates a notification for the following day&apos;s lunch.
3. If the day is Saturday, it sends out a notification with all the lunches for following week.

&lt;strong&gt;Configuration bits:&lt;/strong&gt;

- &lt;strong&gt;&lt;code&gt;TIMEZONE&lt;/code&gt;:&lt;/strong&gt; If you&apos;re in a different timezone, you will probably want to update this in the script.
- &lt;strong&gt;&lt;code&gt;NTFY_TOPIC&lt;/code&gt;:&lt;/strong&gt; This is an environment variable and comes from the topic name from step 1, &lt;code&gt;{school}_lunch_menu&lt;/code&gt;.

&lt;strong&gt;The script:&lt;/strong&gt;

&lt;code&gt;javascript import menu from &quot;./menu.json&quot; with { type: &quot;json&quot; };  const TIMEZONE = &quot;America/Los_Angeles&quot;;  // Helper function to format dates cleanly function getFormattedDates(dateObj) {   const year = dateObj.getFullYear();   const month = String(dateObj.getMonth() + 1).padStart(2, &quot;0&quot;);   const day = String(dateObj.getDate()).padStart(2, &quot;0&quot;);    const friendly = new Intl.DateTimeFormat(&quot;en-US&quot;, {     weekday: &quot;short&quot;,     month: &quot;short&quot;,     day: &quot;numeric&quot;,   }).format(dateObj);    return { iso: `${year}-${month}-${day}`, friendly }; }  export default {   async scheduled(event, env, ctx) {     const NTFY_TOPIC = env.NTFY_TOPIC;      // Get current time in Pacific Time     const now = new Date();     const ptString = now.toLocaleString(&quot;en-US&quot;, { timeZone: TIMEZONE });     const ptDate = new Date(ptString);      const dayOfWeek = ptDate.getDay(); // 0 = Sunday, 1 = Monday ... 6 = Saturday      let message = &quot;&quot;;     let title = &quot;School Lunch Alert&quot;;      if (dayOfWeek === 6) {       // It is Saturday: Grab the menu for next week       title = &quot;Next Week&apos;s Lunch Menu&quot;;       let weeklyMenu = [];        // Loop from Monday (2 days from Sat) to Friday (6 days from Sat)       for (let i = 2; i &amp;lt;= 6; i++) {         const targetDate = new Date(ptDate);         targetDate.setDate(ptDate.getDate() + i);          const { iso, friendly } = getFormattedDates(targetDate);          if (menu[iso]) {           weeklyMenu.push(`- ${friendly}: ${menu[iso]}`);         }       }        // Join the array into a multiline string       if (weeklyMenu.length &amp;gt; 0) {         message = weeklyMenu.join(&quot;\n&quot;);       }     } else {       // It is Sunday-Thursday: Grab the menu for tomorrow       const tomorrow = new Date(ptDate);       tomorrow.setDate(ptDate.getDate() + 1);        const { iso, friendly } = getFormattedDates(tomorrow);        if (menu[iso]) {         message = `${friendly}: ${menu[iso]}`;       }     }      // If we built a message (either daily or weekly), send it     if (message) {       const response = await fetch(`https://ntfy.sh/${NTFY_TOPIC}`, {         method: &quot;POST&quot;,         body: message,         headers: {           Title: title,           Tags: &quot;school_satchel,plate_with_cutlery&quot;,           Priority: &quot;3&quot;,         },       });        if (response.ok) {         console.log(`Sent alert:\n${message}`);       } else {         console.error(`Failed to send alert: ${await response.text()}`);       }     } else {       console.log(&quot;Checked dates: No meals found.&quot;);     }   }, }; &lt;/code&gt;

## Step 6: Create the script

Open a terminal on your computer and navigate to a directory you want the script to live in and initialize a new Worker project.

&lt;code&gt;sh # Move to my documents cd ~/Documents  # Create a new project npm create cloudflare@latest -- lunchbot-js &lt;/code&gt;

You&apos;ll be asked to answer a few questions:

1. What would you like to start with?
- &lt;code&gt;Hello World example&lt;/code&gt;
2. Which template would you like to use?
- &lt;code&gt;Scheduled Worker (Cron Trigger)&lt;/code&gt;
3. Which language do you want to use?
- &lt;code&gt;JavaScript&lt;/code&gt;
4. Do you want to add an AGENTS.md file...?
- &lt;code&gt;No&lt;/code&gt; (or Yes, doesn&apos;t matter)
5. Do you want to use git for version control?
- &lt;code&gt;Yes&lt;/code&gt;
6. Do you want to deploy your application?
- &lt;code&gt;No&lt;/code&gt;

## Step 7: Add your code

&lt;code&gt;cd&lt;/code&gt; into your new project or open the directory with your code editor of choice.

Update the &lt;code&gt;src/index.js&lt;/code&gt; file with the code from &lt;a href=&quot;#step-5-write-the-script&quot;&gt;Step 5: Write the script&lt;/a&gt;.

Inside the &lt;code&gt;src&lt;/code&gt; directory (folder) create a new file named &lt;code&gt;menu.json&lt;/code&gt;. Copy+paste the data from &lt;a href=&quot;#step-4-get-the-data&quot;&gt;Step 4: Get the data&lt;/a&gt;. The script will read the menu from this file.

Update the cron triggers in the &lt;code&gt;wrangler.jsonc&lt;/code&gt; file. This is the new trigger data:

&lt;code&gt;jsonc &quot;triggers&quot;: { 		// Schedule cron triggers: 		&quot;crons&quot;: [ 			// 1. Runs Sun, Mon, Tue, Wed, Thu evenings (PT) 			// (This is 1:00 AM UTC on Mon, Tue, Wed, Thu, Fri) 			&quot;0 1 * * 1-5&quot;, 			// 2. Runs Saturday mornings at 8:30 AM summer / 7:30 AM winter (PT) 			// (This is 3:30 PM UTC on Saturday) 			&quot;30 15 * * 6&quot; 		] 	} &lt;/code&gt;

Deploy the script:

&lt;code&gt;sh npm run deploy &lt;/code&gt;

Set the &lt;code&gt;NTFY_TOPIC&lt;/code&gt; environment variable:

&lt;code&gt;sh npx wrangler secret put NTFY_TOPIC  # You will be prompted to enter the &apos;{school}_lunch_menu&apos; topic you created. &lt;/code&gt;

&lt;strong&gt;🤞 it should all work!&lt;/strong&gt;

## Step 8: Local testing

If something failed, or you want to test it out on your computer, you might want to test it on your computer.

Set the local environment variable by creating a file named &lt;code&gt;.dev.vars&lt;/code&gt; that has this:

&lt;code&gt;sh NTFY_TOPIC=&quot;{school}_lunch_menu&quot; &lt;/code&gt;

Start the local dev server:

&lt;code&gt;sh npm run dev &lt;/code&gt;

You can press &amp;lt;kbd&amp;gt;b&amp;lt;/kbd&amp;gt; on your keyboard or navigate to the url you see in the CLI window. It will look like &lt;code&gt;http://localhost:8787&lt;/code&gt;.

On that page, it&apos;ll have instructions on how to trigger your dev worker.

&amp;gt; To test the scheduled handler, ensure you have used the &quot;--test-scheduled&quot; then try running &quot;curl http://localhost:8787/__scheduled?cron=&lt;em&gt;+&lt;/em&gt;+&lt;em&gt;+&lt;/em&gt;+*&quot;.&lt;img src=&quot;https://sglmr.goatcounter.com/count?p=/blog/rss/lunchbot&quot; alt=&quot;&quot; /&gt;</content:encoded></item><item><title>Notes from building my first Workday Sudio DIS</title><link>https://sglmr.com/blog/workday-dis-notes/</link><guid isPermaLink="true">https://sglmr.com/blog/workday-dis-notes/</guid><pubDate>Sat, 05 Sep 2026 12:00:00 GMT</pubDate><content:encoded>This week, I had a good use case using a custom Data Initialization Service (DIS) in a Studio from &quot;scratch&quot;, and I learned a few things along the way. It was a bit different than using DIS in a connector, &lt;a href=&quot;/blog/workday-integration-dis-is-fast/&quot;&gt;Workday&apos;s Data Initialization Service (DIS) is fast!&lt;/a&gt;.

## What is DIS?

DIS is a way for Workday to query data efficiently. It can act as a substitute for GET_* API calls and RaaS reports. It&apos;s similar in complexity to paged SOAP API requests.

Hidden in a Community discussion, I found a &lt;a href=&quot;https://collaborate.workday.com/t5/Platform-and-Product-Extensions/Can-DIS-in-Studio-Replace-OutRest-and-Can-it-Handle-Multiple/m-p/1516383/highlight/true#M123506&quot;&gt;comment&lt;/a&gt; by product manager &lt;a href=&quot;https://resourcecenter.workday.com/en-us/wrc/public-profile.html?id=5017265&quot;&gt;Doug Lee&lt;/a&gt;, where I learned:

1. The data querying and extraction happens in the tenant before integration code is ran.
2. Queries make better use of resources and typically yield higher performance than web services.
3. The DIS extraction happens before an integration runs. Meaning it won’t count against runtime limits.

DIS negatives?

- Higher complexity than a RaaS report.

Workday Community has a post in the knowledge base with 2 sample .clar files showing how to process DIS in a Studio. &lt;a href=&quot;https://community-content.workday.com/content/workday-community/en-us/kits-and-tools/products/platform-and-product-extensions/integrations/data-initialization-service-for-custom-integrations.html?lang=en-us&quot;&gt;Data Initialization Service for Custom Integrations&lt;/a&gt;

## Lesson&apos;s learned

### Service configuration notes

- Make the &lt;strong&gt;Wrapper Element Name&lt;/strong&gt; be &lt;code&gt;root&lt;/code&gt;
- Make the &lt;strong&gt;Web Service Alias&lt;/strong&gt; be &lt;code&gt;record&lt;/code&gt; or &lt;code&gt;row&lt;/code&gt;
- &lt;em&gt;I have no idea what &lt;strong&gt;Can Be Relaunched with Completed Documents&lt;/strong&gt; does. It’s absent from Workday Community&apos;s docs when I built this.&lt;/em&gt;

## There could be a lot of documents

Depending on the configured partition size, you can end up with at on of little documents to deal with. Be conscious of how and what you log. In addition to the data docs, there are extra audit files.

The document tag I used to iterate the DIS documents was &lt;code&gt;Data - Partial&lt;/code&gt;.

Helpful mvel commands to help processing the documents:

&lt;code&gt;java // All of these are functions you can drop into an // eval or log step. // They need to occur after a GetIntegratonDocuments component.  // Get a list of document tags props[&apos;docLabels&apos;] = da.allLabels.toString();  // Count the number of documents props[&apos;docCount&apos;] = da.size(); &lt;/code&gt;

### DIS Service configuration quirks

&lt;strong&gt;&lt;em&gt;DO NOT&lt;/em&gt;&lt;/strong&gt; create the DIS service in Workday Studio.

1. Log into the tenant and run the task &lt;strong&gt;Create Integration Data Initialization Service&lt;/strong&gt;
2. In Studio, you want to &lt;code&gt;Create service-reference&lt;/code&gt; and type in the exact &lt;strong&gt;name&lt;/strong&gt; of the service you created.
3. Ignore the &lt;em&gt;&quot;⚠️The entered service name is not defined in the workspace&quot;&lt;/em&gt; warning. Studio will figure it out when you deploy.

If you decide to select &lt;code&gt;Create &apos;data-initialization-service&apos;&lt;/code&gt; in Studio, you&apos;ll spend a lot of time rebuilding your DIS service because Studio will wipe it out every single time you deploy.

Huge thank you to &lt;a href=&quot;https://resourcecenter.workday.com/en-us/wrc/public-profile.html?id=100002150747&quot;&gt;Harshil Tamrakar&lt;/a&gt; on these Community discussions:

- &lt;a href=&quot;https://collaborate.workday.com/t5/Platform-and-Product-Extensions/How-to-prevent-Studio-from-removing-the-Schema-from-the-Data/td-p/797091?lang=en-us&quot;&gt;How to prevent Studio from removing the Schema fro... - Workday Community&lt;/a&gt;
- &lt;a href=&quot;https://collaborate.workday.com/t5/Platform-and-Product-Extensions/How-is-data-returned-from-a-Data-Initialization-Service/td-p/530883?lang=en-us&quot;&gt;How is data returned from a Data Initialization Se... - Workday Community&lt;/a&gt;

### Bonus: Integration retrieval service notes

Retrieval services have the same issue described above. Deploying the service in the studio causes it to be deleted with every deploy. I feel like I should have remembered that... It’s been a long time since I&apos;ve done an inbound document-based Studio. The solution is the same as above. Create the service in the tenant.

Thank you again Doug Lee for confirming in a comment: &lt;a href=&quot;https://collaborate.workday.com/t5/Platform-and-Product-Extensions/Studio-Deployment-erases-all-the-services-setup-in-tenant-for/m-p/468265/highlight/true?lang=en-us#M69219&quot;&gt;Re: Studio - Deployment erases all the services se... - Workday Community&lt;/a&gt;&lt;img src=&quot;https://sglmr.goatcounter.com/count?p=/blog/rss/workday-dis-notes&quot; alt=&quot;&quot; /&gt;</content:encoded></item><item><title>Find Workday REST API url&apos;s</title><link>https://sglmr.com/blog/find-workday-rest-api-urls/</link><guid isPermaLink="true">https://sglmr.com/blog/find-workday-rest-api-urls/</guid><pubDate>Tue, 25 Aug 2026 12:00:00 GMT</pubDate><content:encoded>Today I learned how to find out how to find the REST API URLs in Workday.

&lt;a href=&quot;https://workday.my.site.com/customercenter/article?no=000046180&quot;&gt;WD Community: Guide to Formatting REST API Endpoint URLs&lt;/a&gt;

They follow this format:

&lt;code&gt;https://{baseURL}/ccx/api/{service}/{version}/{tenant}/{endpoint}&lt;/code&gt;

- &lt;strong&gt;baseURL:&lt;/strong&gt; Comes from the &lt;strong&gt;View API Clients&lt;/strong&gt; task via the &lt;em&gt;Workday REST API Endpoint&lt;/em&gt; url.
- &lt;strong&gt;service&lt;/strong&gt;: This is the main heading from the &lt;a href=&quot;https://community.workday.com/sites/default/files/file-hosting/restapi/&quot;&gt;Services Directory&lt;/a&gt;
- &lt;strong&gt;version&lt;/strong&gt;: Documented on services directory next to the service name
- &lt;strong&gt;tenant:&lt;/strong&gt; Tenant ID, ex: &lt;code&gt;company1&lt;/code&gt;&lt;img src=&quot;https://sglmr.goatcounter.com/count?p=/blog/rss/find-workday-rest-api-urls&quot; alt=&quot;&quot; /&gt;</content:encoded></item><item><title>Identifying all the US employee-paid taxes in Workday</title><link>https://sglmr.com/blog/identifying-all-the-us-employee-paid-taxes-in-workday/</link><guid isPermaLink="true">https://sglmr.com/blog/identifying-all-the-us-employee-paid-taxes-in-workday/</guid><pubDate>Tue, 18 Aug 2026 12:00:00 GMT</pubDate><content:encoded>This is a tiny reminder for future me.

The way to identify all the employee-paid US tax deduction codes in Workday is to filter on &lt;strong&gt;Pay Component Groups = &lt;code&gt;Statutory Taxes (EE) [USA]&lt;/code&gt;&lt;/strong&gt;.

If you happen to need a list of all the deduction codes, authorities, rates, etc. Then you&apos;ll have to follow a few steps:

## Step 1: Run the All Deductions Report

Make a copy of the &lt;strong&gt;All Deductions&lt;/strong&gt; delivered report.

Add a filter to only include &lt;strong&gt;&lt;code&gt;Statutory Taxes (EE) [USA]&lt;/code&gt;&lt;/strong&gt;

&lt;code&gt;Groups &amp;gt;&amp;gt; Any in the Selection List  &amp;gt;&amp;gt; Value Specified in this Filter &amp;gt;&amp;gt; Statutory Taxes (EE) [USA]&lt;/code&gt;

Download the report.

Keep these fields:

1. Deduction
2. Workday Code

Delete duplicates.

&lt;em&gt;Note: There may be some inactive ones to delete from the list.&lt;/em&gt;

## Step 2. All Payroll Tax Authorities Report

Run the report &lt;strong&gt;All Payroll Tax Authorities&lt;/strong&gt; to get a list of all the payroll tax authorities and their codes.

Download the report.

Keep these fields:

1. Tax Authority Type
2. State/Province
3. County
4. Tax Authority
5. Payslip Description
6. Payroll Authority Tax Code
7. Political Subdivision code

## Step 3. Run the report All Payroll Tax Data (Max Start Date)

Run the report &lt;strong&gt;All Payroll Tax Data (Max Start Date)&lt;/strong&gt;

For prompts, check the box and &lt;code&gt;ctrl+a&lt;/code&gt; everything.

Download the report.

Delete all the columns after &lt;em&gt;Tax&lt;/em&gt;. You don&apos;t need &lt;em&gt;Start Date&lt;/em&gt;, &lt;em&gt;Inactive&lt;/em&gt;, etc. for now.

Delete all the duplicate rows. (Only like 584 duplicates out of 13k+ rows)

## Step 4. Lookup a bunch of stuff

### Match Deduction Codes

The &lt;em&gt;Tax&lt;/em&gt; column on the &lt;strong&gt;All Payroll Tax Data&lt;/strong&gt; report matches the deduction name from the &lt;strong&gt;All Deductions&lt;/strong&gt; report

XLOOKUP the deduction code from the &lt;strong&gt;All Deductions&lt;/strong&gt; Report into the &lt;strong&gt;All Payroll Tax Data&lt;/strong&gt; report.

This should get a majority of the taxes

### Unmatched Deduction Codes

There will be a lot of unmatched deduction codes on the &lt;strong&gt;All Payroll Tax Data&lt;/strong&gt;.

- Delete any with &lt;code&gt;Employer Paid&lt;/code&gt; in the &lt;em&gt;Tax&lt;/em&gt; name (~100 records).
- Delete any with &lt;code&gt;(ER)&lt;/code&gt; in the &lt;em&gt;Tax&lt;/em&gt; name (~100 records).

### Single Unmatched Codes

For some reason, there are a few tax deductions where the codes aren&apos;t picked up by the &lt;strong&gt;All Deductions&lt;/strong&gt; report. They can be found through the &lt;strong&gt;Integration IDs&lt;/strong&gt; task and picking the option for &lt;code&gt;Deductions (All)&lt;/code&gt;.

| Tax                                                         | Deduction Code |
| ----------------------------------------------------------- | -------------- |
| Federal Withholding (Income Code 15) [USA]                  | W_FIC15        |
| Federal Withholding (Income Code 16) (Effective 2015) [USA] | W_FIC16        |
| WA: Seattle Employee Hours Tax [USA]                        | W_WASEA        |
| OASDI Deferral [USA]                                        | W_OASDEF       |
| OASDI Q3 Deferral Payment [USA]                             | W_OASPQ3       |
| OASDI Q4 Deferral Payment [USA]                             | W_OASPQ4       |
| OASDI Territories Deferral [USA]                            | W_OASDITERDEF  |
| OASDI Tip Tax Deferral [USA]                                | W_TIPDEF       |
| OASDI Tip Tax Q3 Deferral Payment [USA]                     | W_TIPPQ3       |
| OASDI Tip Tax Q4 Deferral Payment [USA]                     | W_TIPPQ4       |
| San Francisco Administrative Office Tax [USA]               | W_SFAOT        |

### Multiple Codes

Workday combined a bunch of taxes in the &lt;em&gt;Tax&lt;/em&gt; column on the &lt;strong&gt;All Payroll Tax Data&lt;/strong&gt;.

You have to filter down to those, and duplicate them, one for each of the taxes listed.

&lt;strong&gt;Ta Da!!!!!! It&apos;s Done! _.-.and it should have been a whole lot easier.__&lt;/strong&gt;&lt;img src=&quot;https://sglmr.goatcounter.com/count?p=/blog/rss/identifying-all-the-us-employee-paid-taxes-in-workday&quot; alt=&quot;&quot; /&gt;</content:encoded></item><item><title>Now (August 2026)</title><link>https://sglmr.com/blog/now-2026-08-15/</link><guid isPermaLink="true">https://sglmr.com/blog/now-2026-08-15/</guid><pubDate>Sat, 15 Aug 2026 12:00:00 GMT</pubDate><content:encoded>## Watching

- &lt;strong&gt;Mickey 17&lt;/strong&gt;... the first non-kids movie I&apos;ve watched in over a year.
- &lt;strong&gt;Ted Lasso&lt;/strong&gt;... season 4
- &lt;strong&gt;Silo&lt;/strong&gt;... season 3

## Reading

- &lt;em&gt;re-reading&lt;/em&gt; &lt;strong&gt;Silo&lt;/strong&gt; Series (1-3) by Hugh Howey
- &lt;strong&gt;Slack&lt;/strong&gt; by Tom DeMarco

## Working on

- Moving this website from Django over to Astro. More details on &lt;a href=&quot;/colophon&quot;&gt;/colophon&lt;/a&gt;.
- Still working at Stryker as a &quot;Workday Architect&quot;. I have about 16-17 &quot;in-flight&quot; integrations I&apos;m working on due between now and late November. Wish me luck.
- Not too many house projects going on - thank goodness.
- Exercise - still trying to run and get to the gym regularly. I don&apos;t have any specific running events planned or that I&apos;m training for.

## Enjoying

- Swimming - The family has been taking advantage of the new pool and swimming a lot this summer.
- Handwriting notes - I&apos;ve been enjoying handwriting some of my notes. It helps me a lot more with focus than retention. I try to just take down action items and leave things that I might want to search for later in Obsidian.
- Fountain Pens - Feels very nerdy and satisfying to write with cheap fountain pens. 🙏 that one never explodes somewhere inconvenient or that my kids never run off with one.
- Doodling - I&apos;ve been drawing little doodles for my daughter&apos;s lunches and that&apos;s been fun. I haven&apos;t tried to do a whole lot of sketching. that sort of fell off maybe nearly a year ago.&lt;img src=&quot;https://sglmr.goatcounter.com/count?p=/blog/rss/now-2026-08-15&quot; alt=&quot;&quot; /&gt;</content:encoded></item><item><title>Deep Link to a Workday Object</title><link>https://sglmr.com/blog/deep-link-to-a-workday-object/</link><guid isPermaLink="true">https://sglmr.com/blog/deep-link-to-a-workday-object/</guid><pubDate>Tue, 21 Jul 2026 12:00:00 GMT</pubDate><content:encoded>Today I learned that you can generate a link to a Workday objects based on the WID.

The URL format is like this:

&lt;code&gt;text https://&amp;lt;workday-host&amp;gt;/&amp;lt;tenant&amp;gt;/d/inst/deeplink/&amp;lt;Integration_Event_WID&amp;gt;.htmld &lt;/code&gt;

This could link to a worker, integration event, etc.

&lt;em&gt;When I have some extra time, it would be nice to write a quick Python script that could launch integrations automatically, poll for the results, and then do some kind of pop-up notification when the integration is complete.&lt;/em&gt;&lt;img src=&quot;https://sglmr.goatcounter.com/count?p=/blog/rss/deep-link-to-a-workday-object&quot; alt=&quot;&quot; /&gt;</content:encoded></item><item><title>Fitness watches are like Tamagotchi&apos;s...</title><link>https://sglmr.com/blog/fitness-watches-are-like-tamagotchis/</link><guid isPermaLink="true">https://sglmr.com/blog/fitness-watches-are-like-tamagotchis/</guid><pubDate>Thu, 18 Jun 2026 12:00:00 GMT</pubDate><content:encoded>&amp;gt; Fitness watches are like Tamagotchi&apos;s, except the needy creature you are trying to keep alive is yourself.

&lt;em&gt;Saw on Instagram, I have no idea who to credit it to&lt;/em&gt;&lt;img src=&quot;https://sglmr.goatcounter.com/count?p=/blog/rss/fitness-watches-are-like-tamagotchis&quot; alt=&quot;&quot; /&gt;</content:encoded></item><item><title>Allocate more memory for Workday Studio</title><link>https://sglmr.com/blog/allocate-more-memory-for-workday-studio/</link><guid isPermaLink="true">https://sglmr.com/blog/allocate-more-memory-for-workday-studio/</guid><pubDate>Mon, 15 Jun 2026 12:00:00 GMT</pubDate><content:encoded>Workday Studio has been slow as heck on the Windows computer at my current company. It has been particularly slow at dealing with large XML files.

When I check the task manager in my computer, Studio isn&apos;t using nearly as much memory as I have available... why?

Well, turns out, there are some setting you can change.

&amp;gt; I don&apos;t know yet if this really make a significant difference or is worth the effort. Big XML files still crash after the change. I can see the additional memory usage in Task Manager, but otherwise, &lt;em&gt;shrug&lt;/em&gt;.

## Default settings

If you can dig up the installation location of Workday Studio, somewhere in there is a file named &lt;strong&gt;&lt;code&gt;eclipse.ini&lt;/code&gt;&lt;/strong&gt;. There&apos;s a whole lot of stuff in there, but there are a few important lines:

&lt;code&gt;ini -Xms512m -Xmx4096m &lt;/code&gt;

- &lt;code&gt;-Xms512m&lt;/code&gt; means Studio starts with 512 MB of memory.
- &lt;code&gt;-Xmx4096&lt;/code&gt; means Studio maxes out at 4 GB of memory.

## New settings

With a modern computer, you could try out some more aggressive settings:

&lt;code&gt;ini -Xms2g -Xmx8g &lt;/code&gt;

- &lt;strong&gt;&lt;code&gt;-Xms4g&lt;/code&gt;&lt;/strong&gt;: Changed from &lt;code&gt;512m&lt;/code&gt; to &lt;code&gt;2g&lt;/code&gt;. Studio will now take 2 GB at launch.
- &lt;strong&gt;&lt;code&gt;-Xmx8g&lt;/code&gt;&lt;/strong&gt;: Changed from &lt;code&gt;4096m&lt;/code&gt; to &lt;code&gt;8g&lt;/code&gt;. This doubles the max ceiling to 8gb. &lt;em&gt;If you have 32 GB+ of ram, you could even try 12 GB with &lt;code&gt;-Xmx12g&lt;/code&gt;.&lt;/em&gt;&lt;img src=&quot;https://sglmr.goatcounter.com/count?p=/blog/rss/allocate-more-memory-for-workday-studio&quot; alt=&quot;&quot; /&gt;</content:encoded></item><item><title>World&apos;s easiest steak dinner</title><link>https://sglmr.com/blog/world-s-easiest-steak-dinner/</link><guid isPermaLink="true">https://sglmr.com/blog/world-s-easiest-steak-dinner/</guid><pubDate>Fri, 29 May 2026 12:00:00 GMT</pubDate><content:encoded>My wife and I alternate planning and cooking meals for most days of the week. Friday is almost always steak night because &lt;strong&gt;(A)&lt;/strong&gt; it&apos;s good and &lt;strong&gt;(B)&lt;/strong&gt; it eliminates the &quot;what should we cook?&quot; decision fatigue.

The first few times were a stressful balancing act to not overcook grilling steaks amongst all the fun, demands, and responsibilities that come with raising tiny humans. Thanks to sous vide cooking, we&apos;ve got this more or less down to a mostly hand-off science that feels more like celebrating the end of the week after the kids are in bed.

## Ingredients

- &lt;strong&gt;Steak(s)&lt;/strong&gt; &lt;em&gt;1- to 2-inch, optionally frozen steaks. Season with your favorite seasoning, we usually just use Salt, Pepper, Garlic&lt;/em&gt;
- &lt;strong&gt;Potatoes&lt;/strong&gt; &lt;em&gt;Plus anything you like to mix in like cheese, green onions, sour cream, bacon, etc.&lt;/em&gt;

## Game plan for a 7:30 PM dinner

| Time     | Task                  | Details                                                         |
| :------- | :-------------------- | :-------------------------------------------------------------- |
| &lt;strong&gt;4:15&lt;/strong&gt; | &lt;strong&gt;Preheat Sous Vide&lt;/strong&gt; | Set to preferred temp (e.g., 130°F/ 54°C).                      |
| &lt;strong&gt;4:30&lt;/strong&gt; | &lt;strong&gt;Drop the Steaks&lt;/strong&gt;   | Submerge steaks.                                                |
| &lt;strong&gt;6:15&lt;/strong&gt; | &lt;strong&gt;Oven On&lt;/strong&gt;           | Preheat the oven for potatoes 400°F/200°C.                      |
| &lt;strong&gt;6:30&lt;/strong&gt; | &lt;strong&gt;Potatoes In&lt;/strong&gt;       | Place scrubbed &amp;amp; pricked potatoes on the rack.                  |
| &lt;strong&gt;7:15&lt;/strong&gt; | &lt;strong&gt;Steaks Out&lt;/strong&gt;        | Remove from sous vide bath; pat &lt;strong&gt;very dry&lt;/strong&gt; with paper towels. |
| &lt;strong&gt;7:25&lt;/strong&gt; | &lt;strong&gt;The Sear&lt;/strong&gt;          | Sear in a hot pan (1-2 minutes per side).                       |
| &lt;strong&gt;7:30&lt;/strong&gt; | &lt;strong&gt;Dinner Time&lt;/strong&gt;       | Pull potatoes and serve.                                        |

## Additional Notes

### Sous Vide Doneness Chart

| Doneness           | Temperature                     | Description                            |
| :----------------- | :------------------------------ | :------------------------------------- |
| &lt;strong&gt;Rare&lt;/strong&gt; ⚠️        | &lt;strong&gt;49°C - 51°C&lt;/strong&gt; (120°F - 124°F) | Bright red, cool-to-warm center.       |
| &lt;strong&gt;Medium-Rare&lt;/strong&gt; 🟢 | &lt;strong&gt;54°C - 55°C&lt;/strong&gt; (130°F - 131°F) | Pink-to-red center.                    |
| &lt;strong&gt;Medium&lt;/strong&gt; ⚠️      | &lt;strong&gt;57°C - 60°C&lt;/strong&gt; (135°F - 140°F) | Warm pink throughout; slightly firmer. |
| &lt;strong&gt;Medium-Well&lt;/strong&gt; 💀 | &lt;strong&gt;63°C - 65°C&lt;/strong&gt; (145°F - 150°F) | Slightly pink in the very center.      |
| &lt;strong&gt;Well Done&lt;/strong&gt; 💀💀 | &lt;strong&gt;71°C+&lt;/strong&gt; (160°F+)              | Little to no pink; firm texture.       |&lt;img src=&quot;https://sglmr.goatcounter.com/count?p=/blog/rss/world-s-easiest-steak-dinner&quot; alt=&quot;&quot; /&gt;</content:encoded></item><item><title>Lemon parmesean couscous</title><link>https://sglmr.com/blog/lemon-parmesean-couscous/</link><guid isPermaLink="true">https://sglmr.com/blog/lemon-parmesean-couscous/</guid><pubDate>Mon, 04 May 2026 12:00:00 GMT</pubDate><content:encoded>Couscous is a fast, easy side to throw together while other things are cooking.

## Ingredients

- 1 cup Regular/Moroccan Couscous (dry)
- 1 cup Boiling Water or Broth
- 1 tbsp Butter or Extra Virgin Olive Oil
- ½ Lemon (zested and juiced)
- ¼ cup Grated Parmesan cheese
- Salt and Pepper to taste
- &lt;em&gt;Optional:&lt;/em&gt;
- &lt;em&gt;*Use chicken or vegetable broth for more flavor.&lt;/em&gt;
- &lt;em&gt;Fresh parsley or chives to top while serving.&lt;/em&gt;

## Instructions

1. &lt;strong&gt;Cook:&lt;/strong&gt; Place the dry couscous in a heat-proof bowl. Pour the boiling water (or broth) over the top. Add a tiny pinch of salt and a drizzle of oil/butter.
2. &lt;strong&gt;Wait:&lt;/strong&gt; Cover the bowl with a lid or a plate. Let it sit undisturbed for 5 minutes.
3. &lt;strong&gt;Fluff:&lt;/strong&gt; Remove the lid and use a fork to gently fluff the couscous.
4. &lt;strong&gt;Fold:&lt;/strong&gt; Fold in the lemon zest, lemon juice, and Parmesan cheese. Season with a generous crack of black pepper.
5. &lt;strong&gt;Serve.&lt;/strong&gt;

## Notes

- &lt;strong&gt;Zest first, juice second:&lt;/strong&gt; It is easier to zest a whole lemon than a squeezed-out half.
- &lt;strong&gt;Toast it:&lt;/strong&gt; If you have an extra 2 minutes, toast the dry couscous in a pan with a little butter until it smells nutty before adding the water.
- &lt;strong&gt;Pearl Couscous:&lt;/strong&gt; Increase the water to 1¼ to 1½ cups for pearl couscous. Pearl couscous will need to be simmered for 10-15 minutes to cook.&lt;img src=&quot;https://sglmr.goatcounter.com/count?p=/blog/rss/lemon-parmesean-couscous&quot; alt=&quot;&quot; /&gt;</content:encoded></item><item><title>Static site regrets... I&apos;ve got a few</title><link>https://sglmr.com/blog/static-site-regrets/</link><guid isPermaLink="true">https://sglmr.com/blog/static-site-regrets/</guid><pubDate>Fri, 01 May 2026 12:00:00 GMT</pubDate><content:encoded>It was a little premature to &lt;a href=&quot;/blog/this-site-is-static-now/&quot;&gt;celebrate&lt;/a&gt; moving this site to a static site generator. It&apos;s been all of 2 weeks and I have more than a few regrets...

&lt;strong&gt;I miss writing from anywhere more than I thought I would.&lt;/strong&gt;

I am syncing with Obsidian sync to write content, but I can&apos;t correct and publish it from anywhere. I also don&apos;t have access to the generator everywhere for local previewing. There&apos;s more friction in my writing process than I anticipated.

&lt;strong&gt;I&apos;ve accidentally published draft posts at least 3 times.&lt;/strong&gt;

I don&apos;t think anyone really reads or subscribes to this blog, so it&apos;s probably a non-issue. Regardless, it&apos;s a little embarrassing to accidentally publish incomplete draft posts.

&lt;strong&gt;I miss content organization.&lt;/strong&gt;

The Django admin provided a lot of &quot;free&quot; organization and &quot;CMS&quot;-like capabilities to my writing. I started missing that and researching how I could add a headless CMS to my static site. Before I got very far, I realized... I&apos;m basically rebuilding what I had in Django and picking up more dependencies and &quot;hacks&quot; along the way.

## Back to Django?

I think I&apos;m going to spin up a more basic, vanilla Django proof of concept so that I can compare them side by side. No NPM tailwind dependencies, no Redis, no PostgreSQL, no Docker, no platforms as a service, etc. Just Django, a few packages to do the basics, and a basic VPN server.&lt;img src=&quot;https://sglmr.goatcounter.com/count?p=/blog/rss/static-site-regrets&quot; alt=&quot;&quot; /&gt;</content:encoded></item><item><title>Check for preview in Workday Studio</title><link>https://sglmr.com/blog/check-for-preview-in-workday-studio/</link><guid isPermaLink="true">https://sglmr.com/blog/check-for-preview-in-workday-studio/</guid><pubDate>Thu, 30 Apr 2026 12:00:00 GMT</pubDate><content:encoded>Today I learned how to check if a Workday studio is running in the preview environment.

Saving this here since I don&apos;t know if I&apos;d ever remember &lt;code&gt;context.customerId&lt;/code&gt; even a few days from now.

&lt;code&gt;java props[&quot;tenant&quot;] = context.customerId;  // true/false if the tenant is preview props[&quot;inPreview&quot;] = (props[&quot;tenant&quot;].toString().indexOf(&quot;preview&quot;) &amp;gt; 0) ? true : false; &lt;/code&gt;&lt;img src=&quot;https://sglmr.goatcounter.com/count?p=/blog/rss/check-for-preview-in-workday-studio&quot; alt=&quot;&quot; /&gt;</content:encoded></item><item><title>Find the algorithm and length of a x509 Key</title><link>https://sglmr.com/blog/get-x509-key-details-with-certutil/</link><guid isPermaLink="true">https://sglmr.com/blog/get-x509-key-details-with-certutil/</guid><pubDate>Tue, 28 Apr 2026 12:00:00 GMT</pubDate><content:encoded>Today I learned how to get the algorithm and length of an x509 key (since I couldn&apos;t find it on Workday Community).

(I&apos;m using a Windows 10 computer in the &lt;code&gt;cmd&lt;/code&gt; terminal)

&lt;code&gt;sh certutil -dump &quot;Downloads/key.txt&quot; &lt;/code&gt;

And out of that, I got a whole bunch of junk with these useful bits:

&lt;code&gt;text X509 Certificate: Version: 3 Signature Algorithm:     Algorithm ObjectID: ... Sha256RSA     Algorithm Parameters:     05 00 Public Key Length: 2048 bits &lt;/code&gt;&lt;img src=&quot;https://sglmr.goatcounter.com/count?p=/blog/rss/get-x509-key-details-with-certutil&quot; alt=&quot;&quot; /&gt;</content:encoded></item><item><title>Goodbye afternoon coffee 👋</title><link>https://sglmr.com/blog/goodbye-afternoon-coffee/</link><guid isPermaLink="true">https://sglmr.com/blog/goodbye-afternoon-coffee/</guid><pubDate>Mon, 27 Apr 2026 12:00:00 GMT</pubDate><content:encoded>My 2x/day latte routine is killing me.

This post marks what is probably the 5th time I&apos;ve tried giving up my coffee routine in as many years. And this time is going to be different, maybe? 🤔 🤞

## Why?

&lt;strong&gt;I&apos;m tired of being tired.&lt;/strong&gt; I&apos;ve been reading about how caffeine affects the body and it explains a lot of what I&apos;m feeling. It&apos;s a slow, cumulative effect that sneaks up on me.

&amp;gt; Story time:
&amp;gt; Imagine you strap on a weighted vest every day and go out for a run. And each day, you add a few pounds of weight to the pack. You can do it for a while, but eventually you&apos;ll collapse.

## The revolving door

I&apos;ve got a pretty consistent routine down. It goes something like this:

1. I&apos;ll drink 1 morning latte for a while.
2. Eventually a few bad nights of sleep with the kids or a particularly hard work week leads to an, &lt;em&gt;&quot;I need a break,&quot;&lt;/em&gt; afternoon latte.
3. Then 2x/day turns into a daily habit, with no noticeable downsides.
4. A few months later, I hit a wall.
- I have trouble falling asleep before 10pm.
- I can&apos;t sleep in past 5am (sometimes even earlier).
- The 2x/day latte&apos;s aren&apos;t carrying me through the day the way they used to.&lt;img src=&quot;https://sglmr.goatcounter.com/count?p=/blog/rss/goodbye-afternoon-coffee&quot; alt=&quot;&quot; /&gt;</content:encoded></item><item><title>This site is static now</title><link>https://sglmr.com/blog/this-site-is-static-now/</link><guid isPermaLink="true">https://sglmr.com/blog/this-site-is-static-now/</guid><description>The history of this site before switching to a homemade static site generator.</description><pubDate>Sat, 18 Apr 2026 12:00:00 GMT</pubDate><content:encoded>After 4 years of Django, this site is now built with a DIY static site generator. I set out to write about the new static version, but I never got past the site&apos;s history. I &lt;s&gt;have to&lt;/s&gt; maybe do a part 2 someday to cover the new static site generator.

## 4 years of (mostly) Django

First off, just want to say that Django is awesome and I love it. I&apos;ve dabbled in Go, Flask, FastAPI, and Starlette. None of them let you build things as quickly as you can with Django. You might not always love the Django way or need all its batteries, but they are there when you do. If you ever need to reach for a database, you&apos;re going to have to build from scratch a lot of things Django gives you out of the box.

## Beginning with Django

4 years ago, I wanted to learn how to build &quot;real websites&quot;. So I ran through a few different tutorials. I&apos;m pretty sure I started with the &lt;a href=&quot;https://tutorial.djangogirls.org/en/&quot;&gt;Django Girls tutorial&lt;/a&gt; and then moved onto &lt;a href=&quot;https://wsvincent.com/projects/&quot;&gt;Will Vincent&apos;s books&lt;/a&gt;. From there I needed a real project. So of course I wrote a blog. Somehow I skipped the obligatory, &quot;I started a blog,&quot; post.

The website also served as a hub for many other projects (Django apps):

- &lt;a href=&quot;https://jrnl.sh/en/stable/&quot;&gt;jrnl&lt;/a&gt; from anywhere
- My own &lt;a href=&quot;https://simplenote.com&quot;&gt;Simplenote&lt;/a&gt; (with extra features)
- Link shortener
- Various habit trackers
- Gym log
- Intermittent fasting tracker

### SQLite, PostgreSQL, or &quot;¿Por qué no los dos?&quot;

I started with SQLite and in 2024, moved onto PostgreSQL for the full text search features. &lt;a href=&quot;/blog/django-blog-postgres-search-notes/&quot;&gt;I wrote a bit about it.&lt;/a&gt; Note: SQLite does have FTS, but Django&apos;s ORM doesn&apos;t have as many (if any) features for it.

PostgreSQL has always been a bit stressful for me. I&apos;ve successfully restored a database backup (at least locally), but I never felt that comfortable doing it or that I had a bullet-proof backup strategy. With SQLite, I could just download the database as a file and quickly drop it into my project or a backup drive. &lt;a href=&quot;/blog/django-export-sqlite-database-view/&quot;&gt;I wrote about that too.&lt;/a&gt;

### Django deployment

At first, Django deployment is a circus. Everyone seems to do it a little different. I tried maybe 4 different tutorials, they would all differ a bit from my project in some way. I bet it took two weeks of working on it in the evenings to get it working right with static and media files served correctly. Now I could do it in half a day. It was very frustrating and discouraging, but, even a blind squirrel finds an acorn once in a while.

&lt;em&gt;I think the #1 issue would be linux file permissions, followed by some kind of networking issue (a unix socket, reverse proxy, firewall, etc). After struggling through a few times, I got a feel for how and why different pieces fit together and it got easier to Google the correct incantation to solve any issue. Now, AI is amazing at solving these problems.&lt;/em&gt;

&lt;strong&gt;My last and favorite way to deploy was self-hosting Dokploy on a VPS.&lt;/strong&gt;

To get there, I tried Heroku, Digital Ocean, Linode, Hetzner, Lightsail, Render, Railway, Fly.io and Appliku.

Why so many? I like to tinker, it would usually go something like this:

&amp;gt; Step 1: Learn to deploy on a VPS because it&apos;s cheap.
&amp;gt; Step 2: What if something goes wrong? I&apos;ll try out a PaaS.
&amp;gt; Step 3: Eh, I want to use SQLite (or save money or host multiple projects or...). Back to a VPS.
&amp;gt; Step 4: Rinse and repeat.

## Static curious

At some point in those years, I had been interested in static sites and explored building my content with Pelican and Hugo. I can&apos;t remember if I ever actually deployed any of them. I stayed (or boomeranged) back to Django because it was nice to be able to edit my stuff on a phone or any computer anywhere. I couldn&apos;t really do that with a static site. (You can, but it&apos;s less convenient)

## The Go phase

For fun, I tried to learn Go and loved a lot of things about it.

- It&apos;s boring in mostly the best ways.
- You get so many great tools out of the box with Go that you have to assemble together in Python.
- Compiling any Go project I worked on was still faster than starting up a Python app.
- The programs run faster than Python.
- Static typing is awesome... it made me realize VSCode had superpowers I never knew existed compared to when I was writing Python.
- It &lt;em&gt;feels&lt;/em&gt; productive.

So as I was learning Go, I picked up &lt;a href=&quot;https://lets-go.alexedwards.net&quot;&gt;Let&apos;s Go&lt;/a&gt; and learned how to build a Go web app the Go way. Then I tried rebuilding my whole blog and app the Go way. And again, I played with SQLite, PostgreSQL, sqlc vs stdlib vs pgx, different hosting providers, etc.

My website ran on Go for maybe, 9 months? Eventually it came to an end. Writing Go code felt productive and efficient, but wasn&apos;t. It took so much longer to prototype ideas in Go than with Django. As a Dad with young kids, that was important. It was frustrating that it would take me 1-2 weeks to wire something up in Go that could be as fast as 2 nights in Django.&lt;img src=&quot;https://sglmr.goatcounter.com/count?p=/blog/rss/this-site-is-static-now&quot; alt=&quot;&quot; /&gt;</content:encoded></item><item><title>Django health status notifications with ntfy.sh</title><link>https://sglmr.com/blog/ntfy-django-status/</link><guid isPermaLink="true">https://sglmr.com/blog/ntfy-django-status/</guid><pubDate>Thu, 02 Apr 2026 12:00:00 GMT</pubDate><content:encoded>There are a gazillion ways to get status notifications for a web application, but here&apos;s another one using ntfy.sh

This little helper function would need to be scheduled to run in a task queue like &lt;a href=&quot;https://django-q2.readthedocs.io&quot;&gt;django-q2&lt;/a&gt;. It could also be modified slightly into a cron job.

&lt;code&gt;python def ntfy_status() -&amp;gt; str:     &quot;&quot;&quot;Checks the health check endpoint and sends a notification to ntfy.sh.&quot;&quot;&quot;     import requests     from django.contrib.sites.models import Site     from django.urls import reverse      if not settings.NTFY_TOPIC:         raise ValueError(&quot;NTFY_TOPIC environment variable is not configured.&quot;)      # Create the full url to a health check     protocol = &quot;https&quot; if settings.SECURE_SSL_REDIRECT else &quot;http&quot;     domain = Site.objects.get_current().domain     path = reverse(&quot;health_check&quot;)     url = f&quot;{protocol}://{domain}{path}&quot;      health_response = requests.get(url)      if health_response.status_code == 200:         emoji = &quot;green_circle&quot;         suffix = &quot;is up!&quot;     else:         emoji = &quot;red_circle,skull&quot;         suffix = &quot;is down!&quot;      # Send ntfy message     ntfy_response = requests.post(         f&quot;https://ntfy.sh/{settings.NTFY_TOPIC}&quot;,         data=f&quot;{domain} {suffix}&quot;,         headers={             &quot;Tags&quot;: f&quot;{emoji},{health_response.status_code}&quot;,             &quot;Actions&quot;: &quot;view, Open URL, {url}, clear=true&quot;,         },     )      ntfy_response.raise_for_status()     return &quot;Sent notification to ntfy.sh with status code: &quot; + str(ntfy_response.status_code) &lt;/code&gt;

&lt;img src=&quot;./ntfy-django-status-screenshot.jpeg&quot; alt=&quot;Notification screenshot&quot; /&gt;&lt;img src=&quot;https://sglmr.goatcounter.com/count?p=/blog/rss/ntfy-django-status&quot; alt=&quot;&quot; /&gt;</content:encoded></item><item><title>I ran a half marathon! 🏃</title><link>https://sglmr.com/blog/first-half-marathon-report/</link><guid isPermaLink="true">https://sglmr.com/blog/first-half-marathon-report/</guid><pubDate>Sat, 21 Mar 2026 12:00:00 GMT</pubDate><content:encoded>I don&apos;t think anyone else will ever read or see this, but it was a lot of work; and 6 months ago, I didn&apos;t think I could do it.

&lt;img src=&quot;./shamrockn-half-marathon-2026-finish-line.webp&quot; alt=&quot;Finish line on the warning track&quot; /&gt;

My goal was just to finish. Based on my training runs, it looked like I might run a 2:45:00 time, but I beat it by about 30 minutes. I was trying to run by heart rate, but it felt boringly slow. A few miles in, I switched to a 90s/30s run walk and it carried me through the end of the race.

&lt;img src=&quot;./shamrockn-half-marathon-2026-finisher.jpeg&quot; alt=&quot;&quot; /&gt;

Lesson&apos;s learned:

- A half marathon is very do-able.
- Nature&apos;s Bakery fig bars are decent real food option for runs.
- I drink a lot of water and it was VERY helpful to keep a 500 mL bottle of Skratch with me in a belt during the run.
- The Galloway run walk method was amazing! I think the run would have been a lot harder and slower without it.&lt;img src=&quot;https://sglmr.goatcounter.com/count?p=/blog/rss/first-half-marathon-report&quot; alt=&quot;&quot; /&gt;</content:encoded></item><item><title>Workday Compensation Plan Assignments Security gotcha</title><link>https://sglmr.com/blog/workday-compensation-by-organization-security/</link><guid isPermaLink="true">https://sglmr.com/blog/workday-compensation-by-organization-security/</guid><pubDate>Fri, 06 Mar 2026 12:00:00 GMT</pubDate><content:encoded>I hit another Workday security &quot;gotcha&quot; today.

I needed a field that was based on a related business object &quot;Compensation Plan Assignments&quot;.

No problem, &lt;em&gt;Related Actions &amp;gt;&amp;gt; View Security for Calculated Field&lt;/em&gt;. Okay, &lt;code&gt;View&lt;/code&gt; access on &lt;code&gt;Worker Data: Compensation by Organization&lt;/code&gt;, great.

And it didn&apos;t work! I was stuck for the better part of an hour.

Eventually I came up with the correct search encantation to find something helpful on Workday Community:

&lt;a href=&quot;https://workday.my.site.com/customercenter/article?no=000006862&amp;amp;redirect=false&quot;&gt;Report User Or ISU Not Seeing Merit, Bonus Or Stock Plan Assignment Data&lt;/a&gt;

&amp;gt; &lt;strong&gt;Issue&lt;/strong&gt;
&amp;gt;
&amp;gt; A report user or ISU account is not seeing merit, bonus or stock plan assignment data.
&amp;gt;
&amp;gt; For example &lt;em&gt;Compensation Plan Assignment&lt;/em&gt; related fields, secured by the &lt;em&gt;Worker Data: Compensation by Organization&lt;/em&gt; domain, are not returning the expected merit, bonus or stock plan assignment data.
&amp;gt;
&amp;gt; &lt;strong&gt;Cause&lt;/strong&gt;
&amp;gt;
&amp;gt; In line with your Organizations compensation security model please verify if access is also required on the &lt;em&gt;Worker Data: Funded Plan Assignments&lt;/em&gt; domain:
&amp;gt;
&amp;gt; &lt;em&gt;Worker Data: Funded Plan Assignments&lt;/em&gt;
&amp;gt;
&amp;gt; Domain description: &lt;em&gt;This domain works in conjunction with segment security to provide the ability to secure funded plan (merit, bonus or stock) assignments.&lt;/em&gt;
&amp;gt;
&amp;gt; &lt;strong&gt;Resolution&lt;/strong&gt;
&amp;gt;
&amp;gt; Grant the user access to the applicable &lt;em&gt;Worker Data: Funded Plan Assignments&lt;/em&gt; domain.  Activate changes and retest.
&amp;gt;
&amp;gt; Example impacted fields:
&amp;gt;
&amp;gt; - Compensation Plan Assignment
&amp;gt; - Merit Plans - Plan Details&lt;img src=&quot;https://sglmr.goatcounter.com/count?p=/blog/rss/workday-compensation-by-organization-security&quot; alt=&quot;&quot; /&gt;</content:encoded></item><item><title>Workday Studio &quot;exit()&quot;</title><link>https://sglmr.com/blog/exit-workday-studio/</link><guid isPermaLink="true">https://sglmr.com/blog/exit-workday-studio/</guid><pubDate>Mon, 16 Feb 2026 12:00:00 GMT</pubDate><content:encoded>Today I learned there is an MVEL expression do do a &quot;System.exit&quot; or &quot;sys.exit()&quot; in Workday Studio.

&lt;code&gt;java context.setAbort(true) &lt;/code&gt;

&lt;strong&gt;Important&lt;/strong&gt;: The default behavior of all local-out steps is &lt;code&gt;Propogate Abort = true&lt;/code&gt;. Generally you&apos;ll want this. If the abort isn&apos;t working as expected, check that setting first.

# Example

## 1. Set an integration time limit

Somewhere near the beginning of the integration, set a time limit on the integration.

&lt;code&gt;java  // You could simply set a global value props[&quot;runtimeLimitHours&quot;] = 3.9  // OR you could retrieve it from a launch parameter or integration attribute props[&quot;runtimeLimitHours&quot;] = Double.valueOf(lp.getSimpleData(&quot;Runtime Limit (hours)&quot;)); &lt;/code&gt;

&lt;em&gt;If you use a launch parameter or integration attribute, consider adding an &lt;strong&gt;ValidateExp&lt;/strong&gt; that the value is between 0 and 4 hours&lt;/em&gt;

## 2: Evaluate the run duration

So for example, say you&apos;re in a loop processing a few thousand records. After the splitter, you might have an &lt;strong&gt;eval&lt;/strong&gt; component with something like this:

&lt;code&gt;java // 1. Get the current time and the &apos;sent&apos; time in milliseconds long nowMs = System.currentTimeMillis(); long sentMs = lp.sentOnAsDate.getTime();  // 2. Calculate the difference in milliseconds long diffMs = nowMs - sentMs;  // 3. Convert to hours as a double (to support decimals like 3.75) // There are 3,600,000 milliseconds in one hour double diffHours = diffMs / 3600000.0;  // 4. Logic check if (diffHours &amp;gt; props[&quot;runtimeLimitHours&quot;]) {     // Logic for when the time limit is exceeded     props[&quot;abortIntegration&quot;] = true; } else {     props[&quot;abortIntegration&quot;] = false; } &lt;/code&gt;

## 3. Route by duration

Then you could have a &lt;strong&gt;route&lt;/strong&gt; step that checks &lt;code&gt;props[&quot;abortIntegration&quot;]&lt;/code&gt; for each record. The &lt;em&gt;choose-route&lt;/em&gt; expression would be something like:

&lt;code&gt;java // abort (boolean)props[&quot;abortIntegration&quot;] == true  // continue (boolean)props[&quot;abortIntegration&quot;] == false &lt;/code&gt;

## 4. Handle the abort

The last thing in the logic path will be an &lt;strong&gt;eval&lt;/strong&gt; step with &lt;code&gt;context.setAbort(true)&lt;/code&gt;. Do any steps to close out the integration before the &lt;code&gt;context.setAbort&lt;/code&gt;. So for example, there might be steps like:

1. Log a &lt;code&gt;CRTICICAL&lt;/code&gt; message in a &lt;strong&gt;PutIntegrationMessage&lt;/strong&gt; step
2. Close/save any log files with a &lt;strong&gt;local-out&lt;/strong&gt; step
3. &lt;strong&gt;eval&lt;/strong&gt; step to trigger the abort with &lt;code&gt;context.setAbort(true)&lt;/code&gt;&lt;img src=&quot;https://sglmr.goatcounter.com/count?p=/blog/rss/exit-workday-studio&quot; alt=&quot;&quot; /&gt;</content:encoded></item><item><title>Workday API security gotcha&apos;s</title><link>https://sglmr.com/blog/workday-public-worker-reports-security/</link><guid isPermaLink="true">https://sglmr.com/blog/workday-public-worker-reports-security/</guid><pubDate>Thu, 12 Feb 2026 12:00:00 GMT</pubDate><content:encoded>&amp;gt; &amp;lt;span style=&quot;color:red;&quot;&amp;gt;Validation error occurred. The entered information does not meet the restrictions defined for this field. (Replacement_for_Worker_Reference).&amp;lt;/span&amp;gt;

I was working with the Edit_Job_Requisition API and kept hitting this error; even though I wasn&apos;t touching the &lt;code&gt;Replacement_for_Worker_Reference&lt;/code&gt; at all in my API request.

&lt;strong&gt;Solution: give the ISU &lt;code&gt;View&lt;/code&gt; access to &lt;em&gt;Worker Data: Public Worker Reports&lt;/em&gt;.&lt;/strong&gt; I wasn&apos;t missing any job requisition related security, I was missing the security to see the worker in the &lt;code&gt;Replacement_for_Worker_Reference&lt;/code&gt;. Imagine completing the task through Workday&apos;s UI, I couldn&apos;t see the drop down list of workers without that domain.&lt;img src=&quot;https://sglmr.goatcounter.com/count?p=/blog/rss/workday-public-worker-reports-security&quot; alt=&quot;&quot; /&gt;</content:encoded></item></channel></rss>