NAME
App::karr::Task - Task object representing a single kanban card
VERSION
version 0.500
SYNOPSIS
my $task = App::karr::Task->new(
id => 1,
title => 'Fix login bug',
);
$task->save('/tmp/karr-materialized/tasks');
my $same = App::karr::Task->from_file('/tmp/karr-materialized/tasks/001-fix-login-bug.md');
DESCRIPTION
App::karr::Task models a single task card and knows how to translate between the in-memory object and the Markdown plus YAML frontmatter format used on disk and in Git refs. The same Markdown document is stored in refs/karr/tasks/*/data and in temporary task files that commands materialize while they run.
SEE ALSO
karr, App::karr, App::karr::BoardStore, App::karr::Git, App::karr::Config
id
The task's numeric identifier, immutable once set (is => 'ro'). For a task created through karr create this comes from "allocate_next_id" in App::karr::BoardStore, which hands out the next free integer atomically; "from_file" and "from_string" instead take whatever id the parsed frontmatter carries. Feeds both "filename" (zero-padded to three digits) and the ref path refs/karr/tasks/ID/data that App::karr::Git writes to.
title
The task's display title, required at construction. Lowercased and slugified by "slug" to build "filename", so renaming the title changes the filename that "save" would use when given a $dir; "save" called with no $dir keeps rewriting whatever "file_path" already points at, even after a rename.
status
The task's current lifecycle column, defaulting to backlog when not given. This default is a fixed fallback for direct construction rather than a board-aware one: karr create resolves its own default from the materialized board config ($self->status // $defaults->{status} // 'backlog') before this attribute's default is ever reached. App::karr::Task itself does not validate status against the board's configured statuses -- that is "validate_status" in App::karr::Config's job, called by the commands that accept a --status option.
priority
The task's priority level, defaulting to medium. Same relationship to board config as "status": this default is the fallback for direct construction, karr create resolves its own default from $defaults->{priority} first, and validation against the board's configured priority names happens in "validate_priority" in App::karr::Config, not here. karr pick ranks candidates by it via the board's own priorities list ("priorities" in App::karr::Config), with the most urgent priority being the last entry in the list.
assignee
Optional free-text "who this task belongs to", set via karr create --assignee or karr edit --assignee and filtered on by karr list --assignee. Distinct from "claimed_by": this is a human-maintained note with no expiry, while "claimed_by" is the machine-enforced pick/claim lock that karr pick times out on its own.
tags
Arrayref of free-text tag strings, defaulting to []. BUILD guarantees it is always an arrayref after construction -- a scalar frontmatter value here (tags: urgent instead of a YAML list) is rejected as a usage error at parse time rather than dying later at a dereference in "to_frontmatter" (ticket #125). Every CLI surface that accepts tags (create, edit, pick --tags, list --tag) does its own comma-splitting; this attribute only ever holds the already-split list.
due
Optional due date, stored as the bare YYYY-MM-DD string kanban-md's date.Date expects. App::karr::Task does not validate the format itself -- "validate_due" in App::karr::Config does, called by karr create/edit before the value ever reaches here. Read by karr context's overdue section, compared against today's date as a string.
estimate
Optional free-text time estimate, set via karr create --estimate. Kept verbatim with no parsing or validation anywhere in karr; karr show prints it as given.
class
The task's class of service, defaulting to standard. Same relationship to board config as "status" and "priority": this default is a fixed fallback, karr create resolves its own default from $defaults->{class} first, and "validate_class" in App::karr::Config does the validation. karr pick ranks candidates by it, ahead of priority, via the board's own classes list ("classes" in App::karr::Config), with the most urgent class being the first entry.
parent
Optional parent-task id, round-tripped through the frontmatter like any other modelled field. Nothing in karr currently sets or reads it: no command offers a --parent option, and no filtering, rendering, or dependency logic consults it. A document carrying a parent key survives a karr write unchanged, but today it is inert data as far as karr's own commands are concerned.
depends_on
Arrayref of task ids this task depends on, defaulting to [], normalized the same way as "tags" (a lone scalar value is a parse-time usage error, ticket #125). karr create --depends-on validates every id against the board before the new task's own id is allocated, so a rejected create burns no id (ticket #124, under the same ordering rule as #54); karr move/edit --status/pick warn -- but do not block -- when a task is taken up while a dependency listed here has not yet reached one of the board's terminal statuses (App::karr::Role::DependencyCheck, ticket #123).
body
The Markdown body below the frontmatter delimiters, defaulting to ''. Included in "to_markdown" and "to_json_hash" only when it has non-zero length -- tested by length, not truth, so a body of literal "0" is still a body (ticket #78).
created
Full YYYY-MM-DDTHH:MM:SSZ timestamp set once at construction and never changed again (is => 'ro'). Contrast with "updated", which starts at the same value but moves on every later write.
updated
Full timestamp, defaulting to the same "now" as "created" at construction. App::karr::Task itself never bumps this; "save_task" in App::karr::BoardStore and "save_task_cas" in App::karr::BoardStore do, stamping "now" on every write to a ref that already exists, so a brand-new task keeps updated equal to created until its first real edit. The restore/import path bypasses the bump entirely (writing via "save_task_ref" in App::karr::Git directly) to preserve a document's original timestamps.
claimed_by
Optional agent name holding a karr pick claim, distinct from "assignee". An empty string is treated as "unclaimed" -- kanban-md's own idiom for omitempty on this field -- and is normalized away to "unset" by BUILD rather than left as a predicate-true, empty-string claim (ticket #98). Set together with "claimed_at" by karr pick and karr edit --claim; cleared together by the same commands' unclaim paths.
claimed_at
Timestamp paired with "claimed_by", stamped when a claim is taken. karr pick compares it against the board's configured claim_timeout to decide whether an existing claim has expired and the task can be picked again.
blocked
Boolean-only blocked flag; the invariant every reader relies on is that has_blocked is true if and only if the task is blocked (never "blocked but false"). Only ever set through "block"/"unblock" or by parsing a document -- writing $task->blocked($reason) directly is exactly the bug ticket #58 fixed. A legacy document with a free-text blocked value (karr up to 0.402) is migrated to the boolean-plus-"block_reason" shape on read.
block_reason
Optional free-text reason paired with "blocked", set via "block" or a parsed document. Deliberately not symmetrical with "unblock": a document that says blocked: false while still carrying a block_reason keeps that reason, because dropping it would be the same silent frontmatter deletion ticket #69 fixed -- only an explicit "unblock" throws the reason away.
started
Full timestamp stamped by "update_timestamps" on the first move out of the board's first configured status, or backfilled to "now" when a task is dragged straight to a terminal status without ever passing through in-progress. Never reset by a later move, including a reopen -- the work did begin then (ticket #68).
Before #68 this was stamped as a bare date, which reads as midnight and so precedes the created of any card filed and begun on the same day. Such a stamp is not fixed on read; App::karr::Cmd::Repair migrates it, and anything measuring a duration from it has to reckon with the ordering until that has been run.
completed
Full timestamp stamped by "update_timestamps" when a task reaches one of the board's terminal statuses. Cleared when the task is reopened (moved back out of a terminal status), but not re-stamped by a later terminal-to- terminal move -- done -> archived keeps the original completion time, a deliberate difference from kanban-md, which re-stamps on every such move (ticket #68).
extra
Frontmatter keys karr does not model, kept verbatim so they survive a write. kanban-md unmarshals into a struct and drops anything unknown; karr does not, because the field it would delete is just as likely to be a hand-written note or a newer kanban-md field as it is to be junk (ticket #69).
Keys are not order-preserved: karr's YAML output is key-sorted, so a passthrough field lands in alphabetical position rather than where the author put it.
my $kept = $task->extra->{custom_field};
file_path
Set by "from_file" and by "save", and has a predicate (has_file_path) but no clearer -- nothing in karr ever needs to forget where a task was last written. Its absence is meaningful: a task that lives only in refs/karr/* and was never materialized to a file has no file_path, and "save" called with no $dir dies rather than guessing one, directing the caller to "save_task" in App::karr::BoardStore instead (ticket #77).
block
$task->block('waiting on the upstream API');
$task->block; # blocked, no reason recorded
Marks the task blocked and records the optional reason, keeping blocked and block_reason consistent. This is the only supported way to set them: writing $task->blocked($reason) is what ticket #58 was about.
unblock
$task->unblock;
Clears the blocked flag and any reason with it.
update_timestamps
$task->update_timestamps( $old_status, $new_status, $first_status, $config );
Maintains started and completed across a status transition, the single place that logic lives (kanban-md keeps it in internal/task/lifecycle.go). $first_status is the board's first configured status; pass undef when the caller has no config to hand and only the terminal-status rules should apply.
$config is the board's App::karr::Config, and it decides which statuses are terminal. Omit it and the default board's done/archived pair decides, which is wrong for any board that names its final column something else -- on such a board nothing is ever stamped completed (a leftover from ticket #67). Every caller that has a config in hand should pass it.
Both stamps are full YYYY-MM-DDTHH:MM:SSZ timestamps like created and updated. Before ticket #68 started was a bare date, which is useless for the cycle-time arithmetic karr metrics is meant to do.
One deliberate difference from kanban-md: it re-stamps completed on every move into a terminal status, so done -> archived overwrites the real completion time. karr sets completed only when it is not already set, so archiving a finished task keeps the date it was actually finished.
slug
my $slug = $task->slug;
Lowercases the title, collapses everything that is not [a-z0-9] to a single dash, and trims leading/trailing dashes, truncating on a word boundary at 50 characters the way kanban-md's GenerateSlug does. Used by "filename" to build the on-disk name; not stored anywhere itself, so a title edit changes the slug -- and so the filename -- on the next "save".
filename
my $name = $task->filename; # '007-fix-login-bug.md'
Returns the on-disk filename this task would use in a materialized file view: the id zero-padded to three digits, a dash, and "slug", matching kanban-md's own ^(\d+)- naming convention. Used by "save" when writing into a directory rather than to an already-known "file_path".
to_frontmatter
my $fm = $task->to_frontmatter;
Returns the task as a plain hash reference in kanban-md's frontmatter shape: id, title, status, priority, created, updated, and class are always present, every other modelled field (assignee, due, claimed_by, and so on) only when its predicate is true, and whatever is left in "extra" fills in the rest verbatim.
This is the one place karr and kanban-md agree on what a frontmatter document looks like. to_markdown feeds the result straight to YAML::XS for the on-disk and ref form; "to_json_hash" layers a body key and a real JSON boolean for blocked on top of it for --json output; karr board --json uses it directly, which is why a board column carries no card bodies.
A modelled field always wins the slot it owns, and a field that has since been cleared cannot be resurrected by a stale copy in extra either -- every key karr models is stripped out of extra before the modelled values are laid on top of what remains.
to_json_hash
my $data = $task->to_json_hash;
Returns the task as a plain hash reference ready for JSON encoding: the frontmatter fields from "to_frontmatter" plus a body key when the task has a non-empty body. Used by every command that emits whole tasks as --json: show, list, pick, handoff, materialize, and import. list joined that set late -- it built its payload from "to_frontmatter" alone and therefore dropped every body until ticket #129.
blocked comes back as a JSON boolean, so an agent parsing --json sees the same true kanban-md emits and never the free-text reason it used to get there (ticket #58). A body of "0" is included, because emptiness is tested by length and not by truth (ticket #78).
to_markdown
my $text = $task->to_markdown;
Renders the task as the Markdown-plus-YAML-frontmatter document stored in refs/karr/tasks/*/data and written by "save": "to_frontmatter" dumped as YAML between --- delimiters, followed by the body. The body is terminated with a single trailing newline, added only when it does not already end in one, to match kanban-md's own writer byte-for-byte.
from_string
my $task = App::karr::Task->from_string($markdown_content);
my $task = App::karr::Task->from_string($markdown_content, repair_frontmatter => 1);
Parses a Markdown-plus-YAML-frontmatter document (the same shape "to_markdown" writes) into a new task object. Dies with Invalid task format when the document has no frontmatter block. Frontmatter keys the class does not model are kept on "extra" rather than dropped (ticket #69).
repair_frontmatter is for a board written before refs/karr/meta/encoding existed: only the frontmatter -- not the body -- went through YAML::XS twice and needs "repair_mojibake" in App::karr::Encoding run over it once to undo the double encoding. Set by "load_task_ref_with_oid" in App::karr::Git from the board's own encoding-version check; nothing else should need to pass it (ticket #53).
from_file
my $task = App::karr::Task->from_file('/path/to/007-fix-login-bug.md');
Reads $file, parses it the same way "from_string" does, and sets "file_path" to it so a later "save" with no directory argument rewrites the same file. Every failure -- an unparseable document, a missing required field -- dies with the file path appended, stripping Moo's own "at ... line N" suffix first so the message names the file that is wrong instead of a line in generated constructor code (ticket #70). Used by karr import to read a whole kanban-md-style tasks/ directory, one file at a time.
save
$task->save($dir); # write as $dir/NNN-slug.md, using the current slug
$task->save; # rewrite the file this task was loaded from
Writes "to_markdown" to disk and records the file written as "file_path". Given $dir, the filename is derived fresh from the current "slug" (so a renamed task moves to a new filename, and can leave the old one behind -- callers that materialize a whole board sweep stale files separately; see "materialize_to" in App::karr::BoardStore). With no $dir, rewrites "file_path" as it stands, so a task loaded via "from_file" saves back to the same name it was read from even after a rename.
Dies -- Task has no file_path; ref-backed tasks must be persisted via BoardStore/save_task -- when called with no $dir on a task that has never had a "file_path", i.e. every task that lives only in refs/karr/* and was never materialized to a file. This is deliberate: the canonical write path for such a task is "save_task" in App::karr::BoardStore, and a silent no-op or an implicit directory guess here would let a card go unwritten instead of failing loudly (ticket #77).
SUPPORT
Issues
Please report bugs and feature requests on GitHub at https://github.com/Getty/karr/issues.
IRC
Join #langertha on irc.perl.org or message Getty directly.
CONTRIBUTING
Contributions are welcome! Please fork the repository and submit a pull request.
AUTHOR
Torsten Raudssus <getty@cpan.org>
COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.