{"id":1707,"date":"2026-09-24T11:46:06","date_gmt":"2026-09-24T10:46:06","guid":{"rendered":"https:\/\/ruby-doc.org\/learn\/?p=1707"},"modified":"2026-09-24T11:56:58","modified_gmt":"2026-09-24T10:56:58","slug":"building-a-small-job-tracker-in-ruby","status":"publish","type":"post","link":"https:\/\/ruby-doc.org\/learn\/building-a-small-job-tracker-in-ruby\/","title":{"rendered":"Building a Small Job Tracker in Ruby"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">A job looks simple on a calendar. There is a date, a customer and something that needs doing. Open the email thread behind the job, though, and things become more complicated. For instance, someone wants to move the appointment. A supplier has not responded. The address in the last message differs from the one on the booking.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That is a good problem <a href=\"https:\/\/railsware.com\/blog\/famous-web-apps-built-with-ruby-on-rails\/\">for a Ruby-based application<\/a>. Create a job tracker that lets a small team see what is booked, what is missing, and who needs to take the next step. The initial release can be basic: a few records, some predefined rules and a reliable list of tasks due on a particular day.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Give the project a concrete use case<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">For this example, assume a coordinator responsible for managing deliveries and meetings with external vendors. One booking might involve <a href=\"https:\/\/www.heavyequipmentshipper.com\/tiny-house-movers\/\">tiny house movers<\/a>, while another concerns equipment delivery or a site visit.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The Ruby application manages these records. Your application keeps track of the vendor\u2019s reference number, records confirmation, and identifies any missing information. Ultimately, deciding how to physically accomplish a move remains with the individuals performing the task.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Be careful when establishing boundaries regarding the type of functionality you want to include in your application. Creating a dashboard displaying unconfirmed appointments is a reasonable first goal. Determining whether all jobs are prepared to proceed automatically would require more information than a date and a status field can provide.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Decide what a job contains<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Create a small set of fields for each job: internal job id, job schedule date, job status, job coordinator assignment, provider reference, and additional notes if needed for any details that don&#8217;t warrant individual fields. Using arrays of hashes provides sufficient flexibility to allow you to access each record in your Ruby application until you clearly determine the behavior of each feature. At that point create a dedicated <code>Job<\/code> class. There is little value in developing an overly complex object hierarchy before you understand the questions your tracker must answer.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">When creating a required field, consider what happens if that field is not included. Without a default argument or block, calling <code><a href=\"https:\/\/stackoverflow.com\/questions\/200195\/how-can-i-determine-the-status-of-a-job\">job.fetch(:status)<\/a><\/code> raises <code>KeyError<\/code> if the key doesn&#8217;t exist. This allows you to identify potential issues early with incomplete data rather than allowing that issue to potentially propagate throughout multiple methods. It does not verify the content of the field. Therefore, even though you provided valid keys (e.g., :status), the value associated with that key could potentially contain <code>nil<\/code> or an unacceptable string.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Also assign each job its own unique identifier. The customer name and\/or address can change. Thus neither should be used as the sole identifying factor for the associated booking and its supporting documentation.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Write down the allowed status changes<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Using a status field incorrectly leads to confusion since everyone uses it differently. By definition, does &#8220;confirmed&#8221; indicate agreement by the customer, agreement by the vendor, or both?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Define a limited vocabulary (e.g., <code>requested<\/code>, <code>confirmed<\/code>, <code>completed<\/code>, <code>cancelled<\/code>) for each possible status of a job and document which transitions are permitted from each status. A <code>requested<\/code> job can either transition to <code>confirmed<\/code> or transition to <code>cancelled<\/code>. A <code>completed<\/code> job must go through a defined error resolution process in order for that incorrect value to be corrected.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Implement checks related to these transitions in methods that can be invoked from any interface. If your web form enforces one rule, but your data importer silently applies another, that creates an inconsistent state.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To avoid encapsulating many decisions within a single method named <code>update_job<\/code>, name your methods according to the actions you intend to perform (e.g., <code>confirm!<\/code>, <code>cancel!<\/code>). The exclamation mark is part of the Ruby method name; it does not automatically enforce validation, mutation or persistence.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Let Enumerable handle the daily list<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Once you&#8217;ve ensured your records have been created uniformly, you&#8217;ll find Ruby&#8217;s collections very helpful.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Use <code>select<\/code> to retrieve jobs that match certain criteria, use <code>sort_by<\/code> to order them based on some value, and use <code>group_by<\/code> to aggregate jobs under common keys. These three operations cover most of the daily questions you&#8217;ll likely face: which jobs remain unconfirmed? What comes next? How much work is assigned to each coordinator?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Consider using these three operations as building blocks for your question-answering code. Be sure to keep each operation easily readable. A colleague should be able to alter the query parameters without having to disentangle a long series of nested blocks.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This approach works well when dealing with small amounts of data that fit entirely in memory. However, when working with databases, filter\/sort at the database level whenever possible. Creating large amounts of temporary storage simply to view today&#8217;s workload creates excessive processing overhead for your application.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Make dates explicit<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">While &#8220;Next Friday&#8221; makes sense during casual conversations, it is also poor stored data.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Determine a specific date format for input and convert accepted values into date objects as they enter the system. Ruby&#8217;s <code>Date.iso8601<\/code>, made available via <code>require \"date\"<\/code>, will parse date strings formatted in accordance with the ISO 8601 standard. If you require users to enter dates in an exact format (<code>YYYY-MM-DD<\/code>), ensure you validate that shape as well; ISO 8601 defines multiple acceptable ways to express dates.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Provide meaningful feedback to users when they submit bad dates. Replacing a user-submitted bad date with today&#8217;s date could inadvertently place a job in an inappropriate category.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You should also decide whether scheduling a job requires only a calendar date or whether an accurate appointment time is also required. Calendar dates do not adequately specify &#8220;at 9 am at the destination.&#8221; Consider specifying the time zone applicable to an appointment when determining whether an appointment time matters so that users can rely on reminder notifications correctly.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Expect two people to edit the same booking<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A shared tracker needs a way to handle overlapping edits. Two coordinators may attempt to modify the same booking while viewing different versions of it. Any subsequent update performed on the stale version of that booking should not silently overwrite previous changes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you implement your interface in Rails, Active Record supports optimistic locking through a <code>lock_version<\/code> column. Attempting to update from an out-of-date version of an object raises an <code>ActiveRecord::StaleObjectError<\/code>, providing your application an opportunity to resolve the conflict.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The visual presentation still requires consideration of what users experience when attempting to complete their modifications. Display that the booking was modified; maintain any pending input; prompt users to examine the current state of the booking.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A raw exception page does little to help users complete their work.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Test the questions people will actually ask<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Test unusual scenarios: a job without a provider reference; an invalid date; a cancelled appointment that must disappear from the active list. Also test that an empty day returns an intelligible result.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Pass in an arbitrary reporting date as an argument into any methods dependent upon the current date. This will enable tests to use a fixed date rather than cause method behavior to change as calendars progress.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">As a final check, manually run a few fictional bookings through your tracking application. Confirm one booking, reschedule another booking and cancel one more booking. Have another user identify what should come next from viewing the screen. If they feel compelled to open the original email thread to understand the status of that booking, then that is the next piece of your Ruby application that you need to enhance.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A job looks simple on a calendar. There is a date, a customer and something that needs doing. Open the email thread behind the job, though, and things become more complicated. For instance, someone wants to move the appointment. A supplier has not responded. The address in the last message differs from the one on [&hellip;]<\/p>\n","protected":false},"author":3,"featured_media":1708,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[19],"tags":[],"class_list":["post-1707","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ruby-projects"],"blocksy_meta":{"styles_descriptor":{"styles":{"desktop":"","tablet":"","mobile":""},"google_fonts":[],"version":7}},"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.5 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Building a Small Job Tracker in Ruby - Ruby-Doc Learn<\/title>\n<meta name=\"robots\" content=\"noindex, nofollow\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Building a Small Job Tracker in Ruby - Ruby-Doc Learn\" \/>\n<meta property=\"og:description\" content=\"A job looks simple on a calendar. There is a date, a customer and something that needs doing. Open the email thread behind the job, though, and things become more complicated. For instance, someone wants to move the appointment. A supplier has not responded. The address in the last message differs from the one on [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/ruby-doc.org\/learn\/building-a-small-job-tracker-in-ruby\/\" \/>\n<meta property=\"og:site_name\" content=\"Ruby-Doc Learn\" \/>\n<meta property=\"article:published_time\" content=\"2026-09-24T10:46:06+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-09-24T10:56:58+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/Building-a-Small-Job-Tracker-in-Ruby.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1672\" \/>\n\t<meta property=\"og:image:height\" content=\"941\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"James Britt\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"James Britt\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"6 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-small-job-tracker-in-ruby\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-small-job-tracker-in-ruby\\\/\"},\"author\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/james-britt\\\/#james-britt\"},\"headline\":\"Building a Small Job Tracker in Ruby\",\"datePublished\":\"2026-09-24T10:46:06+00:00\",\"dateModified\":\"2026-09-24T10:56:58+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-small-job-tracker-in-ruby\\\/\"},\"wordCount\":1223,\"publisher\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-small-job-tracker-in-ruby\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/Building-a-Small-Job-Tracker-in-Ruby.png\",\"articleSection\":[\"Ruby Projects\"],\"inLanguage\":\"en-US\"},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-small-job-tracker-in-ruby\\\/\",\"url\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-small-job-tracker-in-ruby\\\/\",\"name\":\"Building a Small Job Tracker in Ruby - Ruby-Doc Learn\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-small-job-tracker-in-ruby\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-small-job-tracker-in-ruby\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/Building-a-Small-Job-Tracker-in-Ruby.png\",\"datePublished\":\"2026-09-24T10:46:06+00:00\",\"dateModified\":\"2026-09-24T10:56:58+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-small-job-tracker-in-ruby\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-small-job-tracker-in-ruby\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-small-job-tracker-in-ruby\\\/#primaryimage\",\"url\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/Building-a-Small-Job-Tracker-in-Ruby.png\",\"contentUrl\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/Building-a-Small-Job-Tracker-in-Ruby.png\",\"width\":1672,\"height\":941},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-small-job-tracker-in-ruby\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Building a Small Job Tracker in Ruby\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/#website\",\"url\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/\",\"name\":\"Ruby-Doc Learn\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/#organization\",\"name\":\"Ruby-Doc Learn\",\"url\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/ruby-doc-logo-transparent.png\",\"contentUrl\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/ruby-doc-logo-transparent.png\",\"width\":1650,\"height\":305,\"caption\":\"Ruby-Doc Learn\"},\"image\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/#\\\/schema\\\/logo\\\/image\\\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/james-britt\\\/#james-britt\",\"name\":\"James Britt\",\"image\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/jgb_self-portrait-20140914.png\",\"description\":\"Creator and long-term maintainer of Ruby-Doc.org, founder of Neurogami, and author of practical Ruby tutorials for Ruby-Doc Learn.\",\"sameAs\":[\"https:\\\/\\\/jamesbritt.com\\\/\",\"https:\\\/\\\/neurogami.com\\\/\",\"https:\\\/\\\/www.oreilly.com\\\/pub\\\/au\\\/2595\",\"https:\\\/\\\/www.rubyevents.org\\\/profiles\\\/james-britt\"],\"url\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/james-britt\\\/\",\"jobTitle\":\"Ruby developer and writer\",\"knowsAbout\":[\"Ruby programming language\",\"Ruby documentation\",\"JRuby\",\"Open Sound Control\",\"Software development\"],\"affiliation\":{\"@type\":\"Organization\",\"name\":\"Ruby-Doc.org\",\"url\":\"https:\\\/\\\/ruby-doc.org\\\/\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Building a Small Job Tracker in Ruby - Ruby-Doc Learn","robots":{"index":"noindex","follow":"nofollow"},"og_locale":"en_US","og_type":"article","og_title":"Building a Small Job Tracker in Ruby - Ruby-Doc Learn","og_description":"A job looks simple on a calendar. There is a date, a customer and something that needs doing. Open the email thread behind the job, though, and things become more complicated. For instance, someone wants to move the appointment. A supplier has not responded. The address in the last message differs from the one on [&hellip;]","og_url":"https:\/\/ruby-doc.org\/learn\/building-a-small-job-tracker-in-ruby\/","og_site_name":"Ruby-Doc Learn","article_published_time":"2026-09-24T10:46:06+00:00","article_modified_time":"2026-09-24T10:56:58+00:00","og_image":[{"width":1672,"height":941,"url":"https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/Building-a-Small-Job-Tracker-in-Ruby.png","type":"image\/png"}],"author":"James Britt","twitter_card":"summary_large_image","twitter_misc":{"Written by":"James Britt","Est. reading time":"6 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/ruby-doc.org\/learn\/building-a-small-job-tracker-in-ruby\/#article","isPartOf":{"@id":"https:\/\/ruby-doc.org\/learn\/building-a-small-job-tracker-in-ruby\/"},"author":{"@id":"https:\/\/ruby-doc.org\/learn\/james-britt\/#james-britt"},"headline":"Building a Small Job Tracker in Ruby","datePublished":"2026-09-24T10:46:06+00:00","dateModified":"2026-09-24T10:56:58+00:00","mainEntityOfPage":{"@id":"https:\/\/ruby-doc.org\/learn\/building-a-small-job-tracker-in-ruby\/"},"wordCount":1223,"publisher":{"@id":"https:\/\/ruby-doc.org\/learn\/#organization"},"image":{"@id":"https:\/\/ruby-doc.org\/learn\/building-a-small-job-tracker-in-ruby\/#primaryimage"},"thumbnailUrl":"https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/Building-a-Small-Job-Tracker-in-Ruby.png","articleSection":["Ruby Projects"],"inLanguage":"en-US"},{"@type":"WebPage","@id":"https:\/\/ruby-doc.org\/learn\/building-a-small-job-tracker-in-ruby\/","url":"https:\/\/ruby-doc.org\/learn\/building-a-small-job-tracker-in-ruby\/","name":"Building a Small Job Tracker in Ruby - Ruby-Doc Learn","isPartOf":{"@id":"https:\/\/ruby-doc.org\/learn\/#website"},"primaryImageOfPage":{"@id":"https:\/\/ruby-doc.org\/learn\/building-a-small-job-tracker-in-ruby\/#primaryimage"},"image":{"@id":"https:\/\/ruby-doc.org\/learn\/building-a-small-job-tracker-in-ruby\/#primaryimage"},"thumbnailUrl":"https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/Building-a-Small-Job-Tracker-in-Ruby.png","datePublished":"2026-09-24T10:46:06+00:00","dateModified":"2026-09-24T10:56:58+00:00","breadcrumb":{"@id":"https:\/\/ruby-doc.org\/learn\/building-a-small-job-tracker-in-ruby\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/ruby-doc.org\/learn\/building-a-small-job-tracker-in-ruby\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/ruby-doc.org\/learn\/building-a-small-job-tracker-in-ruby\/#primaryimage","url":"https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/Building-a-Small-Job-Tracker-in-Ruby.png","contentUrl":"https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/Building-a-Small-Job-Tracker-in-Ruby.png","width":1672,"height":941},{"@type":"BreadcrumbList","@id":"https:\/\/ruby-doc.org\/learn\/building-a-small-job-tracker-in-ruby\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/ruby-doc.org\/learn\/"},{"@type":"ListItem","position":2,"name":"Building a Small Job Tracker in Ruby"}]},{"@type":"WebSite","@id":"https:\/\/ruby-doc.org\/learn\/#website","url":"https:\/\/ruby-doc.org\/learn\/","name":"Ruby-Doc Learn","description":"","publisher":{"@id":"https:\/\/ruby-doc.org\/learn\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/ruby-doc.org\/learn\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/ruby-doc.org\/learn\/#organization","name":"Ruby-Doc Learn","url":"https:\/\/ruby-doc.org\/learn\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/ruby-doc.org\/learn\/#\/schema\/logo\/image\/","url":"https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/ruby-doc-logo-transparent.png","contentUrl":"https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/ruby-doc-logo-transparent.png","width":1650,"height":305,"caption":"Ruby-Doc Learn"},"image":{"@id":"https:\/\/ruby-doc.org\/learn\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/ruby-doc.org\/learn\/james-britt\/#james-britt","name":"James Britt","image":"https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/jgb_self-portrait-20140914.png","description":"Creator and long-term maintainer of Ruby-Doc.org, founder of Neurogami, and author of practical Ruby tutorials for Ruby-Doc Learn.","sameAs":["https:\/\/jamesbritt.com\/","https:\/\/neurogami.com\/","https:\/\/www.oreilly.com\/pub\/au\/2595","https:\/\/www.rubyevents.org\/profiles\/james-britt"],"url":"https:\/\/ruby-doc.org\/learn\/james-britt\/","jobTitle":"Ruby developer and writer","knowsAbout":["Ruby programming language","Ruby documentation","JRuby","Open Sound Control","Software development"],"affiliation":{"@type":"Organization","name":"Ruby-Doc.org","url":"https:\/\/ruby-doc.org\/"}}]}},"_links":{"self":[{"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/posts\/1707","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/comments?post=1707"}],"version-history":[{"count":2,"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/posts\/1707\/revisions"}],"predecessor-version":[{"id":1710,"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/posts\/1707\/revisions\/1710"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/media\/1708"}],"wp:attachment":[{"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/media?parent=1707"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/categories?post=1707"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/tags?post=1707"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}