{"record":{"id":"bc8ca80fe9205906","repo":"instructure/canvas-lms","slug":"postpolicy-is-missing-or-does-not-respond-to-post-manually","errorCode":null,"errorMessage":"PostPolicy is missing or does not respond to post_manually","messagePattern":"PostPolicy is missing or does not respond to post_manually","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"app/models/scheduled_post.rb","lineNumber":101,"sourceCode":"    forty_minutes_from_now = 40.minutes.from_now\n    run_time = Time.current\n\n    scheduled_posts_to_process = pending_comments_posting(forty_minutes_from_now).or(pending_grades_posting(forty_minutes_from_now))\n    scheduled_posts_to_process = scheduled_posts_to_process.where(id: ids) if ids.present?\n\n    scheduled_posts_to_process\n      .preload(\n        :post_policy,\n        assignment: :context\n      ).find_each do |scheduled_post|\n      post_policy = scheduled_post.post_policy\n      assignment = scheduled_post.assignment\n      course = assignment.context\n\n      # If post_manually is false, skip processing this scheduled post\n      unless post_policy&.post_manually\n        scheduled_post.update(post_grades_ran_at: run_time, post_comments_ran_at: run_time)\n        raise \"PostPolicy is missing or does not respond to post_manually\" if post_policy.nil? || !post_policy.respond_to?(:post_manually)\n      end\n\n      if scheduled_post.should_post_comments_and_grades?\n        # If both are set to the same time, then call process_assignment_posting only\n        progress = course.progresses.new(tag: \"scheduled_post_assignment_grades_and_comments:#{assignment.id}\")\n        process_assignment_posting(assignment:, progress:, run_at: scheduled_post.post_grades_at)\n        scheduled_post.update(post_grades_ran_at: run_time, post_comments_ran_at: run_time)\n        next\n      end\n\n      if scheduled_post.should_post_grades?(forty_minutes_from_now)\n        progress = course.progresses.new(tag: \"scheduled_post_assignment_grades:#{assignment.id}\")\n        process_assignment_posting(assignment:, progress:, run_at: scheduled_post.post_grades_at)\n        scheduled_post.update(post_grades_ran_at: run_time)\n      end\n\n      next unless scheduled_post.should_post_comments?(forty_minutes_from_now)\n","sourceCodeStart":83,"sourceCodeEnd":119,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/models/scheduled_post.rb#L83-L119","documentation":"ScheduledPost processing needs the assignment's PostPolicy to decide whether grades are posted manually. This guard fires when the post_policy is nil (no policy record exists for the assignment/course) or the object does not respond to post_manually, so the scheduled post cannot be safely evaluated. It runs after updating the scheduled_post timestamps so the job will not loop forever on a bad record.","triggerScenarios":"A ScheduledPost job runs for an assignment whose course/assignment PostPolicy record is missing (post_policy nil); a stub/mock or mis-shaped post_policy object without a post_manually method is injected in a test or custom code path; data cleanup removed PostPolicy rows while ScheduledPost rows remain.","commonSituations":"Courses created via API or imports that skipped PostPolicy creation; sharded/multi-tenant environments where the PostPolicy lookup targets the wrong shard and returns nil; specs with double objects that don't implement post_manually.","solutions":["Ensure a PostPolicy exists for the course/assignment (e.g. run the backfill/creation path that guarantees every assignment context has one)","Check the assignment.context resolves to the correct course on the correct shard before processing","If post_manually is false, the code intentionally skips — verify the policy is the real record, not a nil placeholder"],"exampleFix":"// before\nunless post_policy&.post_manually\n  ...\nend\n// after\nunless post_policy&.respond_to?(:post_manually)\n  raise \"PostPolicy missing for assignment #{assignment.id}\"\nend\nunless post_policy.post_manually\n  ...\nend","handlingStrategy":"validation","validationCode":"policy = post_policy || assignment.post_policy\nraise 'missing PostPolicy' unless policy.respond_to?(:post_manually)","typeGuard":"policy.respond_to?(:post_manually)","tryCatchPattern":"begin\n  ScheduledPost.process(...)\nrescue RuntimeError => e\n  alert_missing_post_policy if e.message.include?('PostPolicy')\nend","preventionTips":["Backfill PostPolicy records for all courses/assignments","Watch for nil policies in data-cleanup scripts","Use real records, not bare doubles lacking post_manually, in specs"],"tags":["post-policy","data-integrity","ruby"],"backgroundTag":"missing-configuration","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}