Representing 'Jobs to be Done' in a project - Software Crafts
Title: Representing 'Jobs to be Done' in a project
URL Source: https://softwarecrafts.co.uk/100-words/day-303
Markdown Content: I have had this idea/theory for a while, that most software tools would benefit from a simple built in todo list built into the product directly. The main idea is for users to create jobs themselves or for the system itself to create jobs for the user to complete. The general thesis for this idea comes from a direct reference to the term "Jobs to be done". Any piece of software that gets used, exists to complete a job and do it better than the solution before it.
Well over the last couple of months I have turned this idea into a reality inside Hamilton Rock, or at least the first version of it and so far it seems very promising. The general API design is a few custom signals, a model and some signal reciever functions for the custom signals. The rest of the project interacts solely through the custom signals job_requested and job_completed. Each does as you would expect, job_requested requests a job be created, job_completed complete the related job to the model instance given. The core of the job model has a status, type and a generic foreign key which forms the target related to the job. Jobs can auto-completed when a condition has been met, it's just a matter of firing off the completed signal. Example stub of the API is below.
``` job_requested = Signal() job_completed = Signal()
class Job(models.Model): content_type = models.ForeignKey( ContentType, on_delete=models.CASCADE, help_text=("The type of object this job is related to"), ) object_id = models.PositiveIntegerField( help_text=("The ID of the related object"), ) target = GenericForeignKey("content_type", "object_id")
assigned_to = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="assigned_jobs",
help_text=_("User this job is assigned to (default notification recipient)"),
)
job_type = models.CharField(
max_length=50,
choices=jobTypeChoices.choices,
help_text=_("The type of job"),
)
status = models.CharField(
max_length=20,
choices=StatusChoices.choices,
default=StatusChoices.PENDING,
db_index=True,
help_text=_("Current status of the job"),
)
created_at = models.DateTimeField(
null=True,
blank=True,
help_text=_("When the job was completed or cancelled"),
)
updated_at = models.DateTimeField(
null=True,
blank=True,
help_text=_("When the job was completed or cancelled"),
)
completed_at = models.DateTimeField(
null=True,
blank=True,
help_text=_("When the job was completed or cancelled"),
)
expires_at = models.DateTimeField(
null=True,
blank=True,
db_index=True,
help_text=_("When this job should be auto-cancelled if still pending"),
)
metadata = models.JSONField(
default=dict,
blank=True,
help_text=_("Additional job-specific data"),
)
@receiver(job_requested) def handle_job_requested( # NOQA: PLR0913 sender, target, job_type, metadata=None, idempotent=False, assigned_to=None, **kwargs, ): # Creates a Job pass
@receiver(task_completed) def handle_task_completed(sender, target, task_type, metadata=None, **kwargs): # Completes a Job pass ```
Currently all jobs are created by the system and we have yet to directly expose them to our users, however there have been two interesting insights from where I have used them to date (onboarding & staff pages). First in onboarding it has created a flexible way to add/remove steps for me as a developer and given that completing a job is part of the internal state rather than just a custom form or series of forms, the logic to visualise where users are stalling in our flow becomes easy to visualise. It's how many pending jobs at each step and how long have they been pending?
Second when an handled exception case occurs in the application which requires the attention of a staff user, instead of a log statement needing to be processed by some system and being perhaps overfilled with extra context, or there being extra noise in a developer tool such as Sentry, we simply create a job of the type we need. Then create a staff page to list and handle jobs of this type.
Finally this jobs architecture has enabled a central point to control how we send notifications (emails etc) from the system. Every notification is linked to a Job, which then means it forces notifications to not deal with too many actions at a time and again allows a natural tracking of which notifications have been actioned or not.
In future, I'm likely to expose these jobs to our users more directly, giving them a native solution to spend just the right amount of time in our product and no more, so what we build is simple & useful.
What do you think? Would you like this as a package for you to play with? Let me know and I may just break it out and release it!